Go web编程学习笔记——未完待续

1.

1).GOPATH设置

先设置自己的GOPATH,可以在本机中运行$PATH进行查看:

userdeMacBook-Pro:~ user$ $GOPATH
-bash: /Users/user/go: is a directory

在这可见我的GOPATH是/Users/user/go,并在该目录下生成如下作用的三个子目录:

  • src:存放源代码(比如.go .c .h .s等)
  • pkg:编译后生成的文件(比如.a)
  • bin:编译后生成的可执行文件(为了方便可将此目录加入到$PATH中,本机已添加)

 

2.应用目录结构

然后之后如果想要自己新建应用或者一个代码包都是在src目录下新建一个文件夹,文件夹一般是代码包名称,比如$GOPATH/src/mymath/sqrt.go,在这里,包名就是mymath,然后其代码中的包名写成package mymath,比如:

package mymath 

func Sqrt(x float64) float64{
    z := 0.0
    for i := 0; i < 1000; i++ {
        z -= ( z * z - x ) / ( 2 * x )
    }
    return z
}

当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb,在这里包路径就是github.com/astaxie/beedb,包名称为最后一个目录beedb

 

3.编译应用

假设上面我们建好了自己的mymath应用包,之后的编译安装方法有两种:

  • 一是进入对应的应用包目录,即mymath目录,然后运行go install
  • 二是在任意目录下执行go install mymath

编译安装好后,我们就能够到$GOPATH/pkg/${GOOS}_${GOARCH}目录下看见mymath.a这个应用包

${GOOS}_${GOARCH}是平台名,如mac系统是darwin_amd64,linux是linux_amd64

userdeMacBook-Pro:src user$ cd mymath/
userdeMacBook-Pro:mymath user$ ls
sqrt.go
userdeMacBook-Pro:mymath user$ go install
userdeMacBook-Pro:mymath user$ cd ..
userdeMacBook-Pro:src user$ cd ..
userdeMacBook-Pro:go user$ cd pkg
userdeMacBook-Pro:pkg user$ cd darwin_amd64/
userdeMacBook-Pro:darwin_amd64 user$ ls
golang.org    mymath.a
userdeMacBook-Pro:darwin_amd64 user$ 

 

4.调用应用

然后就是对该应用进行调用

比如我们再新建一个应用包mathapp,创建一个main.go源码:

package main
import(
    "mymath"
    "fmt"
)
func main() {
    fmt.Printf("Hello, world. Sqrt(2) = %v \n", mymath.Sqrt(2))
}

然后进入该应用目录,运行go build来编译程序:

userdeMacBook-Pro:src user$ cd mathapp/
userdeMacBook-Pro:mathapp user$ ls
main.go
userdeMacBook-Pro:mathapp user$ go build
userdeMacBook-Pro:mathapp user$ ls
main.go    mathapp
userdeMacBook-Pro:mathapp user$ 

然后运行该可执行文件,./mathapp,得到返回结果为:

userdeMacBook-Pro:mathapp user$ ./mathapp 
Hello, world. Sqrt(2) = 1.414213562373095 

⚠️

package <pkgName> :用于指明当前文件属于哪个包

package main : 说明该文件是一个可独立执行的文件,它在编译后会产生可执行文件

除了main包外,其他包都会生成*.a文件(也就是包文件),并放在$GOPATH/pkg/${GOOS}_${GOARCH}目录下

每一个可独立执行的go程序中,必定都包含一个package main,在这个main包中必定包含一个入口函数main(),该函数即没有参数,也没有返回值

5.获取远程包

如果你想要获取的是一个远程包,可以使用go get获取,其支持多数的开源社区(如github、googlecode、bitbucket、Launchpad),运行语句为:

go get github.com/astaxie/beedb

go get -u参数可以自动更新包,并且在使用go get时会自动获取该包依赖的其他第三方包

userdeMBP:~ user$ go get github.com/astaxie/beedb
userdeMBP:~ user$ cd go/src
userdeMBP:src user$ ls
mymath    golang.org    mathapp        github.com                
userdeMBP:src user$ cd github.com/
userdeMBP:github.com user$ ls
WeMeetAgain    astaxie        btcsuite    conformal
userdeMBP:github.com user$ cd astaxie/
userdeMBP:astaxie user$ ls
beedb
userdeMBP:astaxie user$ cd ../../..
userdeMBP:go user$ cd pkg
userdeMBP:pkg user$ ls
darwin_amd64
userdeMBP:pkg user$ cd darwin_amd64/
userdeMBP:darwin_amd64 user$ ls
github.com    golang.org    mymath.a
userdeMBP:darwin_amd64 user$ cd github.com/astaxie/
userdeMBP:astaxie user$ ls
beedb.a

通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,如github采用git,googlecode采用hg。因此想要使用哪个平台的代码就要对应安装相应的源码控制工具

上面的代码在本地的代码结构为:

 

go get 本质上可以分成两步:

  • 通过源码工具clone代码到src下面
  • 然后自动执行go install

 使用方法就是:

import github.com/astaxie/beedb

 

2.相关http内容可见go标准库的学习-net/http

 

3.表单学习——form

1)如何处理表单的输入

举例:

 

package main 
import(
    "fmt"
    "net/http"
    "log"
)

func index(w http.ResponseWriter, r *http.Request){
    r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据
    fmt.Println(r.Form)
    fmt.Println(r.PostForm)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法
    for k, v := range r.Form{
        fmt.Println("key :", k)
        fmt.Println("value :", v)
    }

    html := `<html>
    <head>
    <title></title>
    </head>
    <body>
    <form action="http://localhost:9090/login" method="post">
        username: <input type="text" name="username">
        password: <input type="text" name="password">
        <input type="submit" value="login">
    </form>
    </body>
    </html>`
    fmt.Fprintf(w, html) //将html写到w中,w中的内容将会输出到客户端中
}

func login(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    r.ParseForm()
    fmt.Println(r.Form)
    fmt.Println(r.PostForm)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"]) 
    if r.Method == "POST"{
        fmt.Println("username : ", r.Form["username"])
        fmt.Println("password : ", r.Form["password"])
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/login", login)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

 

调用http://localhost:9090/后,浏览器返回:

终端返回:

userdeMBP:go-learning user$ go run test.go
map[]
map[]
path /
scheme 
[]

浏览器访问http://localhost:9090/login后终端变成:

userdeMBP:go-learning user$ go run test.go
map[]
map[]
path /
scheme 
[]
method POST
map[username:[hello] password:[world]]
map[username:[hello] password:[world]]
path /login
scheme 
[]
username :  [hello]
password :  [world]

r.Form里面包含所有请求的参数,比如URL中query-string、POST的数据、PUT的数据

当你URL的query-string字段和POST的字段冲突时,该值会被保存成一个slice存储在一起

比如把index函数中html值中的action改成http://localhost:9090/login?username=allen,如下:

<form action="http://localhost:9090/login?username=allen" method="post">

此时的终端为:

method POST
map[password:[world] username:[hello allen]]
map[password:[world] username:[hello]]
path /login
scheme 
[]
username :  [hello allen]
password :  [world]

可见r.PostForm中不会存放URL中query-string的数据

 

2)对表单的输入进行验证

因为不能够信任任何用户的输入,因此我们需要对用户的输入进行有效性验证

主要有两方面的数据验证:

  • 页面端的js验证(使用插件库,比如ValidationJS插件)
  • 服务器端的验证,这里讲的就是这种

1》必填字段

确保从表单元素中能够得到一个值,如上面例子中的username字段,使用len()获取字符串长度:

if len(r.Form["username"][0]) == 0{
    //如果为0则说明该表单元素中没有值,即为空时要做出什么处理
}
  • 当r.Form中表单元素的类型是空文本框、空文本区域以及文件上传,表单元素为空值
  • 如果类型是未选中的复选框和单选按钮,那么就不会在r.Form中产生相应的条目,用这种方法来验证会报错。所以需要使用r.Form.Get()来获取这类表单元素的值,这样当该字段不存在时会返回。但是这种方法只能获取单个值,如果是map的值,还是要使用上面的方法

2》数字

确保从表单获取的是数字,比如年龄

getInt, err := strconv.Atoi(r.Form.Get("age"))
if err != nil {
    //这就说明数字转化出错了,即输入的可能不是数字,这里进行错误的操作
}
//如果确定是数字则继续进行下面的操作
if getInt > 100{ //判断年龄的大小范围的问题
    
}

还有另一种方法就是使用正则表达式:

if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m{
    //如果没有匹配项,则!m为true,说明输入的不是数字
    return false    
}

 

3》中文

保证从表单中获取的是中文,使用正则表达式

if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m{
    //如果没有匹配项,则!m为true,说明输入的不是中文
    return false    
}

 

4》英文

保证从表单中获取的是英文,使用正则表达式

if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("englishname")); !m{
    //如果没有匹配项,则!m为true,说明输入的不是英文
    return false    
}

 

5》电子邮件

查看用户输入的电子邮件是否正确

if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m{
    //如果没有匹配项,则!m为true,说明输入邮箱格式不对
    return false    
}

 

6》手机号码

if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m{
    //如果没有匹配项,则!m为true,说明输入电话号码格式不对
    return false    
}

 

7》下拉菜单

判断表单的<select>元素生成的下拉菜单中被选中项目是否正确,因为有时黑客会伪造下拉菜单中不存在的值发送给你,比如下拉菜单为:

<select name"fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banana">banana</option>
</select>

验证方法为:

slice := []string{"apple", "pear", "banana"}
for _, v := range slice{
    if v == r.Form.Get("fruit"){
        return true
    }
}
return false

 

8》单选按钮

单选按钮<radio>中只有男=1,女=2两个选项,如何防止传入的值为3等错误值,单选按钮为:

<input type="radio" name="gender" value="1"><input type="radio" name="gender" value="2">女

验证方法:

slice := []int {1,2}
for _, v := range slice{
    if v == r.Form.Get("gender"){
        return true
    }
}
return false

 

9》复选框

选定用户选中的都是你提供的值,不同之处在于接受到的数据是一个slice

<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球

验证:

slice := []string{"football", "basketball", "tennis"}
a := Slice_diff(r.Form["interest"], slice)
if a == nil{//说明接收到的数据中的值都来自slice
    return true
}
return false

 

10》时间和日期

使用time处理包

 

11》身份证号

//验证15位身份证,15位都是数字
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m{
    //如果没有匹配项,则!m为true,说明输入身份证格式不对
    return false    
}
//验证18位身份证,前17位都是数字,最后一位是校验码,可能是数字和X
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m{
    //如果没有匹配项,则!m为true,说明输入身份证格式不对
    return false    
}

 

3)预防跨站脚本

因为现在的网站含有大量动态内容以提高用户体验,动态站点会受到名为“跨站脚本攻击”(即XSS)的威胁,静态站点则不受影响

攻击者会在有漏洞的程序中插入攻击的JavaScript、VBScript、ActiveX或Flash来欺骗用户在这上面进行操作来盗取用户的账户信息、修改用户设置、盗取/污染cookie和植入恶意广告等。

两种防护方法:

  • 验证所有输入数据,即上面2)进行的操作
  • 对所有输出数据进行适当的处理,一防止任何已经注入的脚本在浏览器端运行,这里讲的是这种

该适当的处理使用的是html/template中的函数进行转义:

func HTMLEscape

func HTMLEscape(w io.Writer, b []byte)

函数向w中写入b的HTML转义等价表示。

func HTMLEscapeString

func HTMLEscapeString(s string) string

返回s的HTML转义等价表示字符串。

func HTMLEscaper

func HTMLEscaper(args ...interface{}) string

函数返回其所有参数文本表示的HTML转义等价表示字符串。

Template类型是text/template包的Template类型的特化版本,用于生成安全的HTML文本片段

func New

func New(name string) *Template

创建一个名为name的模板。

func (*Template) Parse

func (t *Template) Parse(src string) (*Template, error)

Parse方法将字符串text解析为模板。嵌套定义的模板会关联到最顶层的t。Parse可以多次调用,但只有第一次调用可以包含空格、注释和模板定义之外的文本。如果后面的调用在解析后仍剩余文本会引发错误、返回nil且丢弃剩余文本;如果解析得到的模板已有相关联的同名模板,会覆盖掉原模板。

func (*Template) ExecuteTemplate

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

ExecuteTemplate方法类似Execute,但是使用名为name的t关联的模板产生输出。

因为HTTP是一种无状态的协议,那么要如何判别是否为同一个用户。一般是使用cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互)

更详细的内容可见go标准库的学习-text/template

 举例:

package main 
import(
    "fmt"
    "net/http"
    "log"
    "html/template"
)

func index(w http.ResponseWriter, r *http.Request){
    r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据
    fmt.Println(r.Form)
    fmt.Println(r.PostForm)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法
    for k, v := range r.Form{
        fmt.Println("key :", k)
        fmt.Println("value :", v)
    }
    fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中
}

func login(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    r.ParseForm()
    if r.Method == "GET"{ //
        html := `<html>
<head>
<title></title>
</head>
<body>
<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
</form>
</body>
</html>`
        t := template.Must(template.New("test").Parse(html))
        t.Execute(w, nil)
    }else{
        fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在终端即客户端输出
        fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))//把r.Form.Get("password")转义之后返回字符串
        template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客户端输出,把r.Form.Get("username")转义后写到w
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/login", login)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

访问http://localhost:9090/

访问http://localhost:9090/login

如果仅传入字符串:

服务端返回:

method POST
username :  hello
password :  allen
map[]
map[]
path /favicon.ico
scheme 
[]

客户端:

 

当时如果username输入的是<script>alert()</script>

客户端返回:

可见html/template包默认帮你过滤了html标签

 

如果你想要内容不被转义,方法有:

1》使用text/template

import (
"text/template"
"os"
)
...
t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`)
err := template.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have benn pwned')</script>")

2》使用html/template,和template.HTML

import (
    "html/template"
    "os"
)
...
t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`)
err := template.ExecuteTemplate(os.Stdout, "T", template.HTML("<script>alert('you have benn pwned')</script>"))

 

4)防止多次递交表单

 解决办法是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该唯一值的表单是否已经提交过,如果是,则拒绝再次提交;如果不是,则处理表单进行逻辑处理。

如果使用的是Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮

比如我们能够使用MD5(时间戳)来获取唯一值,如time.Now().Unix()

举例:

package main 
import(
    "fmt"
    "net/http"
    "log"
    "text/template"
    "crypto/md5"
    "time"
    "io"
    "strconv"
)

func index(w http.ResponseWriter, r *http.Request){
    r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据
    fmt.Println(r.Form)
    fmt.Println(r.PostForm)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法
    for k, v := range r.Form{
        fmt.Println("key :", k)
        fmt.Println("value :", v)
    }
    fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中
}

func login(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    
    if r.Method == "GET"{ //
        html := `<html>
<head>
<title></title>
</head>
<body>
<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="hidden" name="token" value="{{.}}">
    <input type="submit" value="login">
</form>
</body>
</html>`
        crutime := time.Now().Unix()
        h := md5.New()
        io.WriteString(h, strconv.FormatInt(crutime, 10))
        token := fmt.Sprintf("%x", h.Sum(nil))

        t := template.Must(template.New("test").Parse(html))
        t.Execute(w, token)
    }else{
        r.ParseForm()
        token := r.Form.Get("token")
        if token != ""{
            //验证token的合法性
        }else{
            //如果不存在token,则报错
            log.Fatal("not token")
        }
        fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在终端即客户端输出
        fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))
        template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客户端输出
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/login", login)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

浏览器中访问http://localhost:9090/login

可见得到的token时间戳为:"7cf962884609e3810259654d1e766754"

该方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措。但是它不能够排除所有的欺骗性的动机,对此类情况还需要更加复杂的工作

 

5)处理文件上传——大文件

 要使得表单能够上传文件,首先就是要添加form的encrype属性,该属性有三种情况:

  • application/x-www-form-urlencoded : 表示在发送前编码所有字符(默认)
  • multipart/form-data :不对字符编码。在使用包含文件上传控件的表单时,必须使用该值,所以这里设置为它
  • text/plain:空格转换为"+"加号,但不对特殊字符编码

举例:

 通过表单上传文件,在服务器端处理文件

package main 
import(
    "fmt"
    "net/http"
    "log"
    "text/template"
    "crypto/md5"
    "time"
    "io"
    "strconv"
    "os"
)

func index(w http.ResponseWriter, r *http.Request){
    r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据
    fmt.Println(r.Form)
    fmt.Println(r.PostForm)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法
    for k, v := range r.Form{
        fmt.Println("key :", k)
        fmt.Println("value :", v)
    }
    fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中
}

func upload(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    
    if r.Method == "GET"{ //
        html := `<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://localhost:9090/upload" method="post">
    <input type="file" name="uploadfile" />
    <input type="hidden" name="token" value="{{.}}" />
    <input type="submit" value="upload" />
</form>
</body>
</html>`
        crutime := time.Now().Unix()
        h := md5.New()
        io.WriteString(h, strconv.FormatInt(crutime, 10))
        token := fmt.Sprintf("%x", h.Sum(nil))

        t := template.Must(template.New("test").Parse(html))
        t.Execute(w, token)
    }else{
        r.ParseMultipartForm(32 << 20) //表示maxMemory,调用ParseMultipart后,上传的文件存储在maxMemory大小的内存中,如果大小超过maxMemory,剩下部分存储在系统的临时文件中
        file, handler, err := r.FormFile("uploadfile") //根据input中的name="uploadfile"来获得上传的文件句柄
        if err != nil{
            fmt.Println(err)
            return
        }
        defer file.Close()
        fmt.Fprintf(w, "%v", handler.Header)
        f, err := os.OpenFile("./test/" + handler.Filename, os.O_WRONLY| os.O_CREATE, 0666)
        if err != nil{
            fmt.Println(err)
            return
        }
        defer f.Close()
        io.Copy(f, file)
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/upload", upload)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再调用不会再有效果

浏览器中返回handler.Header:

test文件夹中也生成了该传入test.txt的副本:

⚠️如果上面的表单form没有设置enctype="multipart/form-data"就会报错:

Content-Type isn't multipart/form-data

上传文件主要三步处理:

  • 表单中增加enctype="multipart/form-data"
  • 服务器调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
  • 使用r.FormFile获取文件句柄,然后对文件进行存储等处理

客户端上传文件

举例:

package main

import(
    "fmt"
    "net/http"
    "io/ioutil"
    "bytes"
    "mime/multipart"
    "os"
    "io"
)

func postFile(filename string, targetUrl string) error {
    bodyBuf := &bytes.Buffer{}
    bodyWriter := multipart.NewWriter(bodyBuf)//把文件的文本流写入一个缓存中,然后调用http.Post方法把缓存传入服务器

    //关键操作
    fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) //使用给出的属性名和文件名创建一个新的form-data头。
    fmt.Println(fileWriter) //&{0xc00008acc0 false <nil>}
    if err != nil{
        fmt.Println("error writing to buffer")
        return err
    }
    //打开文件句柄操作
    fh, err := os.Open(filename)
    if err != nil{
        fmt.Println("error open file")
        return err
    }
    //复制
    _, err = io.Copy(fileWriter, fh)
    if err != nil{
        return err
    }
    contentType := bodyWriter.FormDataContentType()//返回bodyWriter对应的HTTP multipart请求的Content-Type的值,多以multipart/form-data起始。
    fmt.Println(contentType) //multipart/form-data; boundary=b7c3357b23c6a6697af5810d1c0dc0184912ae24c5f5074db8aae0fe5198
    bodyWriter.Close()

    resp, err := http.Post(targetUrl, contentType, bodyBuf)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    resp_body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }
    fmt.Println(resp.Status) //200 OK
    fmt.Println(string(resp_body)) //map[Content-Disposition:[form-data; name="uploadfile"; filename="./testFmt.txt"] Content-Type:[application/octet-stream]]
    return nil
}
func main() {
    targetUrl := "http://localhost:9090/upload"
    filename := "./testFmt.txt"
    postFile(filename, targetUrl)
}

运行之前服务端的同时调用该客户端,返回如上数据,然后可见相应的test文件夹中生成了testFmt.txt:

 

 4.访问数据库

 1)database/sql接口

更详细的内容可看go标准库的学习-database/sql/drivergo标准库的学习-database/sql

go和PHP不同的地方是Go没有官方提供数据库驱动,而是为开发者开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动。

这样的好处是只要按照标准接口开发的代码,以后需要迁移数据库时,不需要任何更改。

1》sql.Register - 在database/sql中

该函数用来注册数据库驱动。当第三方开发者开发数据库驱动时,都会实现init函数,在init里面调用这个Register(name string, driver driver.Driver)完成本驱动的注册,比如

1>sqlite3的驱动:

//http://github.com/mattn/go-sqlite3驱动
func init(){
    sql.Register("sqlite3", &SQLiteDriver{})
}

2>mysql的驱动

//http://github.com/mikespook/mymysql驱动
var d = Driver{proto : "tcp", raddr : "127.0.0.1:3306"}
func init(){
    Register("SET NAMES utf8")
    sql.Register("mymysql", &d)
}

由上可见第三方数据库驱动都是通过这个函数来注册自己的数据库驱动名称及相应的driver实现。

上面的例子实现的都是注册一个驱动,该函数还能够实现同时注册多个数据库驱动,只要这些驱动不重复,通过一个map来存储用户定义的相应驱动

var drivers = make(map[string]driver.Driver)
drivers[name] = driver

 

在使用database/sql接口和第三方库时经常看见如下:

import(
    "database/sql"
    _ "github.com/mattn/go-sqlite3" //上面定义的sqlite3驱动包
)

里面的_的作用就是说明引入了"github.com/mattn/go-sqlite3"该包,但是不直接使用包里面的函数或变量,其中init函数也不自动调用。因此我们之后需要自己手动去调用init函数。

 

2》driver.Driver - 在database/sql/driver中

Driver是一个数据库驱动的接口,其定义了一个Open(name string)方法,该方法返回一个数据库的Conn接口:

type Driver interface {
    // Open返回一个新的与数据库的连接,参数name的格式是驱动特定的。
    //
    // Open可能返回一个缓存的连接(之前关闭的连接),但这么做是不必要的;
    // sql包会维护闲置连接池以便有效的重用连接。
    //
    // 返回的连接同一时间只会被一个go程使用。
    Open(name string) (Conn, error)
}

因为返回的连接同一时间只会被一个go程使用,所以返回的Conn只能用来进行一次goroutine操作,即不能把这个Conn应用于Go的多个goroutine中,否则会出现错误,如:

 

go goroutineA(Conn) //执行查询操作
go goroutineB(Conn) //执行插入操作

 

这样的代码会使Go不知某个操作到底是由哪个goroutine发起的从而导致数据混乱。即可能会讲goroutineA里面执行的查询操作的结果返回给goroutineB,从而让goroutineB将此结果当成自己执行的插入数据

 

 

3》driver.Conn - 在database/sql/driver中

Conn是一个数据连接的接口定义。这个Conn只能应用在一个goroutine中,如上所说。

type Conn interface {
    // Prepare返回一个准备好的、绑定到该连接的状态。
    Prepare(query string) (Stmt, error)

    // Close作废并停止任何现在准备好的状态和事务,将该连接标注为不再使用。
    //
    // 因为sql包维护着一个连接池,只有当闲置连接过剩时才会调用Close方法,
    // 驱动的实现中不需要添加自己的连接缓存池。
    Close() error

    // Begin开始并返回一个新的事务。
    Begin() (Tx, error)
}

Prepare函数返回与当前连接相关的SQL语句的准备状态,可以进行查询、删除等操作

Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql中建议的conn pool,所以不用再去实现缓存conn之类的,这样会更容易引起问题

Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询、更新等操作,或者对事务进行回滚、递交

 

4》driver.Stmt - 在database/sql/driver中

Stmt是一种准备好的状态,绑定到一个Conn中,并只能应用在一个goroutine中。

type Stmt interface {
    // Close关闭Stmt。
    //
    // 和Go1.1一样,如果Stmt被任何查询使用中的话,将不会被关闭。
    Close() error

    // NumInput返回占位参数的个数。
    //
    // 如果NumInput返回值 >= 0,sql包会提前检查调用者提供的参数个数,
    // 并且会在调用Exec或Query方法前返回数目不对的错误。
    //
    // NumInput可以返回-1,如果驱动占位参数的数量不知时。
    // 此时sql包不会提前检查参数个数。
    NumInput() int

    // Exec执行查询,而不会返回结果,如insert或update。
    Exec(args []Value) (Result, error)

    // Query执行查询并返回结果,如select。
    Query(args []Value) (Rows, error)
}

Close函数关闭当前的连接状态,但是如果当前正在执行query,query还是会有效地返回rows数据

Exec函数执行Conn的Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据

Query函数执行Conn的Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集

 

5》driver.Tx - 在database/sql/driver中

事务处理一般就两个过程,递交或回滚,即下面的两个函数:

type Tx interface {
    Commit() error
    Rollback() error
}

 

6》driver.Execer - 在database/sql/driver中

这是一个Conn可选择实现的接口

type Execer interface {
    Exec(query string, args []Value) (Result, error)
}

如果一个Conn未实现Execer接口,sql包的DB.Exec会首先准备一个查询(即调用Prepare返回Stmt),执行状态(即执行Stmt的Exec函数),然后关闭状态(即关闭Stmt)。Exec可能会返回ErrSkip。

 

7》driver.Result

这是是执行Update/insert等操作返回的结果接口定义

type Result interface {
    // LastInsertId返回insert等命令后数据库自动生成的ID
    LastInsertId() (int64, error)

    // RowsAffected返回被查询影响的行数
    RowsAffected() (int64, error)
}

 

8》driver.Rows

Rows是执行查询返回的结果集接口定义

type Rows interface {
    // Columns返回各列的名称,列的数量可以从切片长度确定。
    // 如果某个列的名称未知,对应的条目应为空字符串。
    Columns() []string

    // Close关闭Rows。
    Close() error

    // 调用Next方法以将下一行数据填充进提供的切片中,即返回下一条数据,并把数据返回给dest。
    // 提供的切片必须和Columns返回的切片长度相同。
    //
    // 切片dest可能被填充同一种驱动Value类型,但字符串除外;即dest里面的元素必须是driver.Vlaue的值,除了string。
    // 所有string值都必须转换为[]byte。
    //
    // 当没有更多行时,Next应返回io.EOF。
    Next(dest []Value) error
}

Columns函数返回查询数据库表的字段信息,返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段

 

9》driver.RowsAffected

type RowsAffected int64

RowsAffected其实就是int64的别名,但是它实现了Result接口,用来底层实现Result的表示方式

RowsAffected实现了Result接口,用于insert或update操作,这些操作会修改零到多行数据。

 

10》driver.Value

type Value interface{}

Value其实就是一个空接口,它可以容纳任何数据

driver.Value是驱动必须能够操作的Value,所以Value要么是nil,要么是下面的任意一种:

int64
float64
bool
[]byte
string   [*] Rows.Next不会返回该类型值
time.Time

 

11》driver.ValueConverter

ValueConverter接口定义了一个如何把一个普通值转化成driver.Value的接口

type ValueConverter interface {
    // ConvertValue将一个值转换为驱动支持的Value类型
    ConvertValue(v interface{}) (Value, error)
}

ValueConverter接口提供了ConvertValue方法。

driver包提供了各种ValueConverter接口的实现,以保证不同驱动之间的实现和转换的一致性。ValueConverter接口有如下用途:

  • 转换sql包提供的driver.Value类型值到数据库指定列的类型,并保证它的匹配,例如保证某个int64值满足一个表的uint16列。
  • 转换数据库提供的值(即数据库查询结果)成driver.Value类型。
  • 在Scan函数中被sql包用于将driver.Value类型转换为用户定义的类型。

 

12》driver.Valuer

type Valuer interface {
    // Value返回一个驱动支持的Value类型值
    Value() (Value, error)
}

Valuer接口定义了一个返回driver.Value的方法

很多类型都实现了这个Value方法,用来实现自身与driver.Value的转换

 

一个驱动driver只要实现了上面的这些接口就能够完成增删改查等基本操作,剩下的就是与相应的数据库进行数据交互等细节问题了

 

2)使用MySQL数据库

1.MySQL驱动

Go中支持MySQL的驱动很多,有些支持database/sql标准,有些采用的是自己的实现接口。常用的有下面的几种:

  • https://github.com/go-sql-driver/mysql,支持database/sql,全部采用go写
  • https://github.com/ziutek/mymysql,支持database/sql,也支持自定义接口,全部采用go写
  • https://github.com/Philio/GoMySQL,不支持database/sql,自定义接口,全部采用go写

在这里我们使用的是第一个驱动

首先可见该驱动源码中mysql/driver.go为:

import (
    "database/sql"
    "database/sql/driver"
    "net"
    "sync"
)
type MySQLDriver struct{}
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    ...
}
...

当第三方开发者开发数据库驱动时,都会实现init函数来完成本驱动的注册,这样才能在Open时使用"mysql"作为其参数driverName的值,说明打开的是上面注册的mysql驱动

首先先在mysql中创建数据库test,并生成两个表,一个是用户表userinfo,一个是关联用户信息表userdetail。使用workbench进行创建,首先创建数据库test:

CREATE SCHEMA `test` DEFAULT CHARACTER SET utf8 ;

然后创建表:

use test;
create table `userinfo` (
    `uid` int(10) not null auto_increment,
    `username` varchar(64) null default null,
    `department` varchar(64) null default null,
    `created` date null default null,
    primary key (`uid`)
);

create table `userdetail`(
    `uid` int(10) not null default '0',
    `intro` text null,
    `profile` text null,
    primary key (`uid`)
);

接下来就示范怎么使用database/sql接口对数据库进行增删改查操作:

当然运行前首先需要下载驱动:

go get -u github.com/go-sql-driver/mysql

举例;

package main 
import(
    "fmt"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func checkErr(err error){
    if err != nil{
        panic(err)
    }
}



func main() {
    db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname"
    defer db.Close()
    checkErr(err)

    //插入数据
    stmt, err := db.Prepare("insert userinfo set username = ?,department=?,created=?")
    checkErr(err)

    //执行准备好的Stmt
    res, err := stmt.Exec("user1", "computing", "2019-02-20")
    checkErr(err)

    //获取上一个,即上面insert操作的ID
    id, err := res.LastInsertId()
    checkErr(err)
    fmt.Println(id) //1

    
    //更新数据
    stmt, err =db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err = stmt.Exec("user1update", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(affect) //1

    //查询数据
    rows, err := db.Query("select * from userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int 
        var username, department, created string
        err = rows.Scan(&uid, &username, &department, &created) //1 user1update computing 2019-02-20
        checkErr(err)
        fmt.Println(uid, username, department, created)
    }
  defer rows.Close() //关闭结果集,释放链接
    //删除数据
    stmt, err = db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err = stmt.Exec(id)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)
    fmt.Println(affect) //1

}

返回:

userdeMBP:go-learning user$ go run test.go
1
1
1 user1update computing 2019-02-20
1

可以知道该操作成功了,但是从workbench中查看userinfo表中没有变化,因为插入的数据又被删除了,因此上面的三步如果一步步来我们就能够看见如下的输出:

1)先插入数据

package main 
import(
    "fmt"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func checkErr(err error){
    if err != nil{
        panic(err)
    }
}



func main() {
    db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname"
    defer db.Close()
    checkErr(err)

    //插入数据
    stmt, err := db.Prepare("insert userinfo set username = ?,department=?,created=?")
    checkErr(err)

    //执行准备好的Stmt
    res, err := stmt.Exec("user1", "computing", "2019-02-20")
    checkErr(err)

    //获取上一个,即上面insert操作的ID
    id, err := res.LastInsertId()
    checkErr(err)
    fmt.Println(id) //2,因为上面进行过一次操作了,这次操作id会自增1,所以为2
    affect, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(affect)//1

}

workbench中可见表中数据为:

2)更改数据

package main 
import(
    "fmt"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func checkErr(err error){
    if err != nil{
        panic(err)
    }
}



func main() {
    id := 2 
    db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname"
    defer db.Close()
    checkErr(err)

    //更新数据
    stmt, err :=db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err := stmt.Exec("user1update", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(affect) //1

    //查询数据
    rows, err := db.Query("select * from userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int 
        var username, department, created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid, username, department, created) //2 user1update computing 2019-02-20
    }
   defer rows.Close() //关闭结果集,释放链接
}

workbench中可见表中数据变为:

3)删除数据

package main 
import(
    "fmt"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func checkErr(err error){
    if err != nil{
        panic(err)
    }
}



func main() {
    id := 2 
    db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname"
    defer db.Close()
    checkErr(err)

    //删除数据
    stmt, err := db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err := stmt.Exec(id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(affect) //1

}

workbench表中数据就被清空了

 

上面代码使用的函数的作用分别是:

1.sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver/mysql中注册了mysql这个数据库驱动,第二个参数是DNS(Data Source Name),它是go-sql-driver/mysql定义的一些数据库连接和配置信息,其支持下面的几种格式:

user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be::ca:fe]:80)/dbname

2.db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态

3.db.Query()函数用来直接执行Sql并返回Rows结果

4.stmt.Exec()函数用来执行stmt准备好的SQL语句

⚠️sql中传入的参数都是=?对应的数据,这样做可以在一定程度上防止SQL注入

 还有调用其他驱动的例子,如sqlite3\Mongodb等,过程大同小异,这里省略

 

5.session和数据存储

因为HTTP协议是无状态的,所以每次请求都是无状态的,因此为了解决在web操作中连接与用户无法关联的问题,提出了cookie和session的解决方案。

cookie机制是一种客户端机制,把用户数据保存在客户端,客户可更改;session机制是一种服务端机制,服务器使用一种类似散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即session ID,该ID的存放方式有两种:一是经过URL,即GET方式传递给服务器,二是保存在客户端的cookies中,通过cookie来获取。当然你也可以将session保存到数据库(如memcache或redis)中,更安全,但是效率会下降。

1)session和cookie

1》cookie

cookie简而言之就是在本地计算机中保存一些用户操作的历史信息(包括登录信息,是一小段文本信息),并且在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证

因此cookie存在一定的安全隐患。例如本地cookie中保存的用户名密码被破译或cookie被其他网站收集

cookie是有时间限制的,根据生命周期的不同分成:

  • 会话cookie:如果不设置过期时间,则默认cookie的生命周期为从创建到关闭浏览器为止。只要关闭了浏览器,cookie则消失。一般不保存在硬盘中,而是保存在内存中
  • 持久cookie:设置了过期时间,该cookie会被保存在硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直至超过设定的过期时间。

存储在硬盘上的cookie能够在不同的浏览器进程之间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式

Go中使用net/http中的SetCookie()函数来设置cookie:

func SetCookie

func SetCookie(w ResponseWriter, cookie *Cookie)

SetCookie在w的头域中添加Set-Cookie头,该HTTP头的值为cookie。

然后使用request的Cookies()、Cookie(name string)函数和response的Cookies()函数来获取设置的cookie信息

func (*Request) Cookies

func (r *Request) Cookies() []*Cookie

Cookies解析并返回该请求的Cookie头设置的cookie。

func (*Request) Cookie

func (r *Request) Cookie(name string) (*Cookie, error)

Cookie返回请求中名为name的cookie,如果未找到该cookie会返回nil, ErrNoCookie。

 

func (*Response) Cookies

func (r *Response) Cookies() []*Cookie

Cookies解析并返回该回复中的Set-Cookie头设置的cookie。

详情可见本博客的go标准库的学习-net/http

 

2》session

session就是在服务器上保存用户操作的历史信息。服务器使用session ID来标识session,session ID有服务器生成,保证其随机性和唯一性,相当于一个随机密钥,避免在握手或者传输中暴露用户真实密码。该session ID会保存在cookie中

使用session过程:

  • 当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里面是否包含了一个session的表示标识,即session ID
  • 如果包含session ID,则说明该用户之前已经登录过该服务器,创建过session,服务器就会根据该session ID把session搜索出来
  • 如果检索不到该session ID对应的session,则会新建一个,出现这种情况的原因是可能服务器已经删除了该用户对应的session对象,但用户认为地在请求的URL后面附上一个JSESSION的参数来传递一个session ID
  • 如果客户的请求中不包含session ID,则为此客户创建一个session并同时生成一个与该session相关联的session ID,该session ID会在本次response响应中返回给客户端保存

 

 

2)Go如何使用session

目前Go标准包中没有为session提供支持,因此这里使用的是作者写的session包,可见:https://github.com/astaxie/session,这个是旧版本,更新版本的可见https://github.com/astaxie/beego/tree/master/session

这里使用的是旧版本,来说明下要如何自定义session包

1》session的创建

  • 生成全局唯一标识符session ID
  • 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统掉电将会丢失所有会话数据。因此最好是将会话数据写到文件里或存储在数据库中,虽然会增加I/O开销,但这样可以实现某种程度的session持久化,也更有利于session的共享
  • 将该session ID发送给客户端

最关键就在于如何发送session ID,有两种常用方法:cookie和URL重写

  • cookie:cookie服务端通过设置Set-cookie头来将session标识符传送到客户端。这样客户端之后的每一次请求就都会带着这个标识符。这种带有session的cookie一般都会将失效时间设置为0,即该cookie为会话cookie
  • URL重写:在返回给用户的页面里的所有URL后面追加session标识符,这样用户在收到响应之后,无论是点击响应页面的哪个链接或者是提交表单,都会自动带上这个session ID,从而实现了会话的保持。这种做法比较麻烦,但是在客户端禁用cookie时,这种方案将是首选

2》Go实现session管理

  •  全局session管理器
  • 保证session的全局唯一性
  • 为每一个顾客关联一个session
  • session的存储(可以存储到内存,文件,数据库等)
  • session过期处理

下面是实现session管理器Manager的代码,该代码主要实现了一下的功能:

  • session ID的生成—sessionId() string:根据随机数生成session ID
  • session的创建—SessionStart(w http.ResponseWriter, r *http.Request) (session Session):判断当前的请求request中是否存在有效的session,有则返回,否则创建
  • session的销毁—SessionDestroy(w http.ResponseWriter, r *http.Request) :销毁session同时删除sessionid
  • session的垃圾回收GC—GC:将到期的session移除

代码为:

package session

import (
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "sync"
    "time"
)

type Session interface {
    Set(key, value interface{}) error //set session value
    Get(key interface{}) interface{}  //get session value
    Delete(key interface{}) error     //delete session value
    SessionID() string                //back current sessionID
}

type Provider interface {
    SessionInit(sid string) (Session, error)
    SessionRead(sid string) (Session, error)
    SessionDestroy(sid string) error
    SessionGC(maxlifetime int64)
}

var provides = make(map[string]Provider) //根据provider的名字字符串来存储对应的Provider

// Register通过提供的名字来使得session的provider了用
// 如果Register被同样的名字调用两次或者驱动driver为nil,则会报错Panic
// 该函数由实现了Provider接口的结构体调用
func Register(name string, provide Provider) {
    if provide == nil {
        panic("session: Register provide is nil")
    }
    if _, dup := provides[name]; dup { //因为如果之前该name没有调用过Register则不会在slice——provides中找到对应的值的
        panic("session: Register called twice for provide " + name)
    }
    provides[name] = provide
}

//定义一个全局的session管理器
type Manager struct {
    cookieName  string     //private cookiename
    lock        sync.Mutex //互斥锁,用来保护session
    provider    Provider   //存储session方式
    maxlifetime int64      //cookie有效期
}

//实例化一个session管理器
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
    provider, ok := provides[provideName]
    if !ok { //说明该provider还没有调用Register函数进行注册
        return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
    }
    //否则就能够使用该provider来生成session管理器
    return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}

//判断当前的请求request中是否存在有效的session,有则返回,否则创建
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    cookie, err := r.Cookie(manager.cookieName) //从请求中获取cookie值
    if err != nil || cookie.Value == "" { //如果没能得到cookie,则创建一个
        sid := manager.sessionId() //首先新创建一个session ID
        session, _ = manager.provider.SessionInit(sid) //在Provider中根据提供的session ID初始化一个session
        //然后将session的唯一标识符sid写到cookie中,这样之后就能够使用它去查看对应的session
        cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
        http.SetCookie(w, &cookie) //然后设置cookie
    } else {//如果有
        sid, _ := url.QueryUnescape(cookie.Value) //QueryUnescape函数用于将QueryEscape转码的字符串还原,QueryEscape函数对string进行转码使之可以安全的用在URL查询里
        session, _ = manager.provider.SessionRead(sid) //从Provider中根据给定的session ID读取session
    }
    return
}

//销毁session同时删除sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {//如果没能得到cookie,则返回,没得可删的session
        return
    } else {//如果有
        manager.lock.Lock()
        defer manager.lock.Unlock()
        manager.provider.SessionDestroy(cookie.Value) //消除Provider中的session
        expiration := time.Now()
        //并将相应cookie对应的session的失效信息返回
        cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
        http.SetCookie(w, &cookie)
    }
}

//将到期的session移除
func (manager *Manager) GC() {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    manager.provider.SessionGC(manager.maxlifetime) //session垃圾回收,即将到期的session移除
    time.AfterFunc(time.Duration(manager.maxlifetime)*time.Second, func() { manager.GC() }) //即隔一段时间进行一次垃圾回收
}

//生成session ID
func (manager *Manager) sessionId() string {
    b := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, b); err != nil { //ReadFull从rand.Reader精确地读取len(b)字节数据填充进b。rand.Reader是一个全局、共享的密码用强随机数生成器
        return ""
    }
    //举例,如当b为[219 189 5 108 128 157 17 143 220 18 92 213 44 54 28 9 128 244 53 239 6 5 67 43 121 235 190 133 241 73 246 10]
    //下面返回的session ID为270FbICdEY_cElzVLDYcCYD0Ne8GBUMreeu-hfFJ9go=
    return base64.URLEncoding.EncodeToString(b)//将生成的随机数b编码后返回字符串
}

 

3)session存储——存储在内存中

下面的代码实现的是将session存储在内存中,如果你想要学习存储在其他地方的实现,可见https://github.com/astaxie/beego/tree/master/session

 实现代码为memory.go:

package memory

import (
    "container/list" //list包实现了双向链表
    "github.com/astaxie/session"
    "sync"
    "time"
)

//该SessionStore实现了session.go中的Session接口
type SessionStore struct {
    sid          string                      //session id唯一标示       
    timeAccessed time.Time                   //最后访问时间       
    value        map[interface{}]interface{} //session里面存储的值
}

//设置
func (st *SessionStore) Set(key, value interface{}) error {
    st.value[key] = value //将该键值对存储到SessionStore的value切片中
    pder.SessionUpdate(st.sid) //更新st.sid对应的session的最后访问时间并将其移到Provider的GC list的第一个位置
    return nil
}

func (st *SessionStore) Get(key interface{}) interface{} {
    pder.SessionUpdate(st.sid)
    if v, ok := st.value[key]; ok {//得到session中key对应的value
        return v
    } else {
        return nil
    }
    return nil
}

func (st *SessionStore) Delete(key interface{}) error {
    delete(st.value, key) //按照指定的键key将元素从映射st.value中删除
    pder.SessionUpdate(st.sid)
    return nil
}

//得到session ID值
func (st *SessionStore) SessionID() string {
    return st.sid
}

//该Provider实现了session.go中的Provider接口,实现session是存储在内存的
type Provider struct {
    lock     sync.Mutex               //用来锁
    sessions map[string]*list.Element //用来存储session对应的内容到内存
    list     *list.List               //用来做gc
}

//在Provider中根据提供的session ID初始化一个session
func (pder *Provider) SessionInit(sid string) (session.Session, error) {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    v := make(map[interface{}]interface{}, 0)
    //根据session生成一个SessionStore对象,timeAccessed为现在生成的时间,value为空列表
    newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
    element := pder.list.PushBack(newsess) //然后将这个新SessionStore存储到pder.list双向列表的尾部,然后会返回一个生成的新元素,该元素的element.Value值就是这个新生成的SessionStore对象指针
    pder.sessions[sid] = element //然后将这个session ID对应的生成的element对应存储在Provider的session列表中
    return newsess, nil //然后返回的是这个新生成的SessionStore对象指针
}

//从Provider中根据给定的session ID读取session
func (pder *Provider) SessionRead(sid string) (session.Session, error) {
    if element, ok := pder.sessions[sid]; ok { //element.Value中存储的就是该session ID 对应的&SessionStore对象指针
        return element.Value.(*SessionStore), nil
    } else { //如果没有,则重新初始化一个session
        sess, err := pder.SessionInit(sid)
        return sess, err
    }
    return nil, nil
}

//消除Provider中的session
func (pder *Provider) SessionDestroy(sid string) error {
    if element, ok := pder.sessions[sid]; ok {
        delete(pder.sessions, sid) //从pder.sessions列表中删除sid对应的session
        pder.list.Remove(element) //同时也将该元素从list列表中删除
        return nil
    }
    return nil
}

//session垃圾回收,即将到期的session移除
func (pder *Provider) SessionGC(maxlifetime int64) {
    pder.lock.Lock()
    defer pder.lock.Unlock()

    for {
        element := pder.list.Back() //Back返回链表最后一个元素
        if element == nil { //直到为nil则说明链表中的element已经获取完了,可以结束循环了
            break
        }
        //查看session的有效时间是否已经到期,将到期的session移除
        if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
            pder.list.Remove(element)
            delete(pder.sessions, element.Value.(*SessionStore).sid)
        } else {
            break
        }
    }
}

//更新某个session的最后访问时间,并将该session移到Provider的GC list的第一个位置
func (pder *Provider) SessionUpdate(sid string) error {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    if element, ok := pder.sessions[sid]; ok { //获得某个session ID的存储内容
        element.Value.(*SessionStore).timeAccessed = time.Now() //将双向链表的一个元素的值转成*SessionStore类型然后设置其最后访问时间
        pder.list.MoveToFront(element) //MoveToFront将元素element移动到链表的第一个位置,说明这是最新操作的值
        return nil
    }
    return nil
}

var pder = &Provider{list: list.New()} //New创建一个链表

func init() {
    pder.sessions = make(map[string]*list.Element, 0)
    session.Register("memory", pder) //调用session.go中的Register函数,将名字为"memory"的Provider-pder注册

调用举例说明如何使用:

package main 
import(
    "fmt"
    "net/http"
    "log"
    "html/template"
    "github.com/astaxie/session"
    _ "github.com/astaxie/session/providers/memory"
)

var globalSessions *session.Manager
var err error

func init(){
    globalSessions, err = session.NewManager("memory","gosessionid",60) //参数分别表示(provideName, cookieName string, maxlifetime int64)
    if err != nil{
        log.Fatal(err)
    }
    go globalSessions.GC()
}

func index(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中
}

func login(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    cookie, _ := r.Cookie("gosessionid")
    fmt.Printf("start before : Name = %s, Value = %s\n", cookie.Name, cookie.Value)
    sess := globalSessions.SessionStart(w, r)
    fmt.Printf("after start: Name = %s, Value = %s\n", cookie.Name, cookie.Value)
    fmt.Printf("Before - session name: %v\n", sess.Get("username"))
    r.ParseForm()
    if r.Method == "GET"{
        html := `<html>
<head>
<title></title>
</head>
<body>
<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
    {{.}}
</form>
</body>
</html>`
        t := template.Must(template.New("test").Parse(html))
        w.Header().Set("Content-Type", "text/html")
        t.Execute(w, nil)
    }else{
        sess.Set("username", r.Form["username"])
        fmt.Printf("After - session name:  %v\n", sess.Get("username"))
        fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在终端即客户端输出
        fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))//把r.Form.Get("password")转义之后返回字符串
        http.Redirect(w, r, "/", 302) //让其上交完表单后就会自动重定向到"/"网址上
        // template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客户端输出,把r.Form.Get("username")转义后写到w
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/login", login)         //设置访问的路由
    err = http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
method GET
start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D //这里有值的原因是因为之前运行过一次,有效时间设置为了3600,按照道理如果是第一次调用,应该为空
after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D
Before - session name: <nil>
method POST
start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D
after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D
Before - session name: <nil>
After - session name:  [hello] //设置session后我们能够看见能够得到username的值
username :  hello
password :  world

 

4)预防session劫持

 在session技术中,客户端和服务端通过session的标识符来维护会话,但这个标识符很容易就能够被嗅探到,从而被其他人利用,这是中间人攻击的一种类型

1>session的劫持

 比如之前我们在chrome浏览器中生成了的name = gosessionid, value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D的

先不打开服务器,仅仅是在Firefox中访问http://localhost:9090/login,当然会报页面连接失败的,然后我们使用Firefox的cookie editor插件来该网址上插入chrome中的cookie值

返回为:

    {
        "name": "gosessionid",
        "value": "qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D",
        "domain": "localhost",
        "hostOnly": true,
        "path": "/",
        "secure": false,
        "httpOnly": false,
        "sameSite": "no_restriction",
        "session": true,
        "firstPartyDomain": "",
        "storeId": null
    },

然后再将服务器打开,然后这时候再在浏览器中访问http://localhost:9090/login时就能够看见返回的结果也是:

userdeMBP:go-learning user$ go run test.go
method GET
start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D
after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D
Before - session name: <nil>

按照道理来说不应该是相同的,这样其实就说明我们成功实现了session的劫持

2>session劫持的预防

 有三种解决方案:

  • 一是session ID的值只允许通过cookie设置,而不是通过URL重置方式设置。同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie。这样子设置的好处一是防止这个cookie被XSS读取从而引起session劫持;二是cookie设置不会像URL重置方式那么容易获取session ID
  • 二是在每一个请求里面添加token,就是之前讲的防止form重复递交类似的功能。在每一个请求中加上一个隐藏的token,然后每次验证这个token,从而保证用户请求的唯一性
h := md5.New()
salt := "some%^7&8888"
io.WriteString(h, salt + time.Now().String())
token := fmt.Sprintf("%x", h.Sum(nil))
if r.Form["token"] != token {
    //提示登录
}
sess.Set("token", token)
  • 三是给session额外设置一个创建时间的值,一旦过了这个时间,就把这个session ID销毁,重新生成新的session,这样可以在一定程度上防止session劫持的问题
createtime := sess.Get("createtime")
if createtime == nil {
    sess.Get("createtime", time.Now().Unix())
}else if(createtime.(int64) + 60) < (time.Now().Unix()){
    sess = globalSessions.SessionStart(w, r)
}

 

6. 文本处理

1)XML处理

XML 被设计用来传输和存储数据,HTML 被设计用来显示数据HTML 旨在显示信息,而 XML 旨在传输信息。

XML 能把数据从 HTML 分离:

如果你需要在 HTML 文档中显示动态数据,那么每当数据改变时将花费大量的时间来编辑 HTML。

通过 XML,数据能够存储在独立的 XML 文件中。这样你就可以专注于使用 HTML 进行布局和显示,并确保修改底层数据不再需要对 HTML 进行任何的改变。

通过使用几行 JavaScript,你就可以读取一个外部 XML 文件,然后更新 HTML 中的数据内容。

属性和元素的区别:

请尽量使用元素来描述数据。而仅仅使用属性来提供与数据无关的信息。元数据(有关数据的数据)应当存储为属性,而数据本身应当存储为元素。

 

1》解析XML—— xml.Unmarshal()

为管理的服务器生成下面内容的XML配置文件:

<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
</servers>

举例:

xml文件为:

<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
</servers>

举例:

package main 
import(
    "fmt"
    "encoding/xml"
    "io/ioutil"
    "os"
    "log"
)
type Recurlyservers struct {//后面的内容是struct tag,标签,是用来辅助反射的
    XMLName xml.Name `xml:"servers"` //将元素名写入该字段
    Version string `xml:"version,attr"` //将version该属性的值写入该字段
    Svs []server `xml:"server"`
    Description string `xml:",innerxml"` //Unmarshal函数直接将对应原始XML文本写入该字段
}

type server struct{
    XMLName xml.Name `xml:"server"`
    ServerName string `xml:"serverName"`
    ServerIP string `xml:"serverIP"`
}
func main() {
    file, err := os.Open("servers.xml")
    if err != nil {
        log.Fatal(err)
    }

    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        log.Fatal(err)
    }

    v := Recurlyservers{}
    err = xml.Unmarshal(data, &v)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(v) 

    fmt.Printf("XMLName: %#v\n", v.XMLName)
    fmt.Printf("Version: %q\n", v.Version)

    fmt.Printf("Server: %v\n", v.Svs)
    for i, svs := range v.Svs{
        fmt.Println(i)
        fmt.Printf("Server XMLName: %#v\n", svs.XMLName)
        fmt.Printf("Server ServerName: %q\n", svs.ServerName)
        fmt.Printf("Server ServerIP: %q\n", svs.ServerIP)        
    }
    fmt.Printf("Description: %q\n", v.Description)

}

返回:

userdeMBP:go-learning user$ go run test.go
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] 
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
}
XMLName: xml.Name{Space:"", Local:"servers"}
Version: "1"
Server: [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
0
Server XMLName: xml.Name{Space:"", Local:"server"}
Server ServerName: "Shanghai_VPN"
Server ServerIP: "127.0.0.1"
1
Server XMLName: xml.Name{Space:"", Local:"server"}
Server ServerName: "Beijing_VPN"
Server ServerIP: "127.0.0.2"
Description: "\n    <server>\n        <serverName>Shanghai_VPN</serverName>\n        <serverIP>127.0.0.1</serverIP>\n    </server>\n    <server>\n        <serverName>Beijing_VPN</serverName>\n        <serverIP>127.0.0.2</serverIP>\n    </server>\n"

 xml解析到struct的与标签tag相关的规则详情可见go标准库的学习-encoding/xml的Unmarshal()函数部分

只要设置对了tag,XML的解析就会变得十分简单,tag和XML的element是一一对应的关系

为了正确解析,go语言的xml包要求struct定义中的所有字段都必须是可导出的,即首字母为大写

 

2》输出XML

如果想要生成上面所示的XML文件,需要使用到xml包中的下面两个函数:

  • func Marshal(v interface{}) ([]byte, error)
  • func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的XML数据流

举例生成上面解析的XML文件:

package main 
import(
    "fmt"
    "encoding/xml"
    "os"
)
type Servers struct {//后面的内容是struct tag,标签,是用来辅助反射的
    XMLName xml.Name `xml:"servers"` //将元素名写入该字段
    Version string `xml:"version,attr"` //将version该属性的值写入该字段
    Svs []server `xml:"server"`
}

type server struct{
    ServerName string `xml:"serverName"`
    ServerIP string `xml:"serverIP"`
}

func main() {
    v := &Servers{Version : "1"}
    v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
    v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
    //每个XML元素会另起一行并缩进,每行以prefix(这里为两个空格)起始,后跟一或多个indent(这里为四个空格)的拷贝(根据嵌套层数)
    //即第一层嵌套只递进四个空格,第二层嵌套则递进八个空格
    output, err := xml.MarshalIndent(v,"  ", "    ")
    if err != nil{
        fmt.Printf("error : %v\n", err)
    }
    os.Stdout.Write([]byte(xml.Header)) //输出预定义的xml头  <?xml version="1.0" encoding="UTF-8"?>
    os.Stdout.Write(output)
}

返回:

userdeMBP:go-learning user$ go run test.go
<?xml version="1.0" encoding="UTF-8"?>
  <servers version="1">
      <server>
          <serverName>Shanghai_VPN</serverName>
          <serverIP>127.0.0.1</serverIP>
      </server>
      <server>
          <serverName>Beijing_VPN</serverName>
          <serverIP>127.0.0.2</serverIP>
      </server>
  </servers>

需要os.Stdout.Write([]byte(xml.Header))这句代码是因为上面的两个函数输出的信息都是不带XML头的,为了生成正确的xml文件,需要使用xml包预定义的Header变量

另一个例子:

package main 
import(
    "fmt"
    "encoding/xml"
    "os"
)
type Address struct {
    City, State string
}
type Person struct {
    XMLName   xml.Name `xml:"person"`   //该XML文件的根元素为person
    Id        int      `xml:"id,attr"` //该值会作为person元素的属性
    FirstName string   `xml:"name>first"` //first为name的子元素
    LastName  string   `xml:"name>last"`  //last
    Age       int      `xml:"age"`
    Height    float32  `xml:"height,omitempty"` //含omitempty选项的字段如果为空值会省略
    Married   bool  //默认为false
    Address         //匿名字段(其标签无效)会被处理为其字段是外层结构体的字段,所以没有Address这个元素,而是直接显示City, State这两个元素
    Comment string `xml:",comment"` //注释
}

func main() {
    v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42}
    v.Comment = " Need more details. "
    v.Address = Address{"Hanga Roa", "Easter Island"}
    output, err := xml.MarshalIndent(v, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    os.Stdout.Write(output)
}

返回:

userdeMBP:go-learning user$ go run test.go
  <person id="13">
      <name>
          <first>John</first>
          <last>Doe</last>
      </name>
      <age>42</age>
      <Married>false</Married>
      <City>Hanga Roa</City>
      <State>Easter Island</State>
      <!-- Need more details. -->
  </person>

如果是用的是xml.Marshal(v),返回为:

userdeMBP:go-learning user$ go run test.go
<person id="13"><name><first>John</first><last>Doe</last></name><age>42</age><Married>false</Married><City>Hanga Roa</City><State>Easter Island</State><!-- Need more details. --></person>

可读性就会变得差很多

 

2)JSON处理

1》解析json文件:

1>解析到结构体

使用的JSON包的方法为:

func Unmarshal

func Unmarshal(data []byte, v interface{}) error

详情可见go标准库的学习-encoding/json

举例:

package main 
import(
    "fmt"
    "encoding/json"
)
type Server struct {
    ServerName string
    ServerIP string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    //json文件的内容
    str := `
{
    "servers" : [
        {
            "serverName" : "Shanghai_VPN",
            "serverIP" : "127.0.0.1"
        },
        {
            "serverName" : "Beijing_VPN",
            "serverIP" : "127.0.0.2"        
        }
    ]
}`
    json.Unmarshal([]byte(str), &s)
    fmt.Println(s)
    for i, svs := range s.Servers {
        fmt.Println(i)
        fmt.Printf("ServerName is %s\n", svs.ServerName)
        fmt.Printf("ServerIP is %s\n", svs.ServerIP)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
{[{Shanghai_VPN 127.0.0.1} {Beijing_VPN 127.0.0.2}]}
0
ServerName is Shanghai_VPN
ServerIP is 127.0.0.1
1
ServerName is Beijing_VPN
ServerIP is 127.0.0.2

⚠️:

能够被赋值的字段必须是可导出字段,即首字母大写

同时JSON解析只会解析能够找得到的字段,如果找不到则忽略。这样的好处是当你接收到一个很大的JSON数据结构并只想要获取其中的部分数据的时候,你只需要将你想要的数据对应的字段名大写,就可以轻动解决这个问题

就比如上面的例子,如果我将结构体中的serverName首字母小写变成:

type Server struct {
    serverName string
    ServerIP string
}

则得到的s将会是:

{[{ 127.0.0.1} { 127.0.0.2}]}

可见不会得到serverName的值

 

2>解析到interface

上面的解析方式是在我们知道被解析的json数据的结构的前提下使用的方法,当不能够确定结构时应该使用interface

JSON包中使用map[string]interface{} 和 []interface{} 结构来存储任意的JSON对象和数组

 Go类型和JSON类型的对应关系是:

  • bool代表JSON booleans
  • float64 代表 JSON numbers
  • string 代表 JSON strings
  • nil 代表 JSON null

1.通过interface{} 与 type assert的配合,这样就能够解析未知结构的JSON数了

举例:

package main 
import(
    "fmt"
    "encoding/json"
)

func main() {
    var s interface{}
    str := `
{
    "name" : "testJSON",
    "servers" : [
        {
            "serverName" : "Shanghai_VPN",
            "serverIP" : "127.0.0.1"
        },
        {
            "serverName" : "Beijing_VPN",
            "serverIP" : "127.0.0.2"        
        }
    ],
    "status" : false
}`
    json.Unmarshal([]byte(str), &s)
    fmt.Println(s)
    //然后需要通过断言来将其从interface{}类型转成map[string]interface{}类型
    m := s.(map[string]interface{})

    for key, value := range m {
        switch v := value.(type){//得到s的类型
        case string :
            fmt.Println(key , " is string ", v)
        case int :
            fmt.Println(key, "is int", v)
        case []interface{}:
            fmt.Println(key, "is an array")
            for i, vv := range v{
                fmt.Println(i, vv)
            }
        default:
            fmt.Println(key, "is of a type i don't know how to handle")
        }
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
map[name:testJSON servers:[map[serverName:Shanghai_VPN serverIP:127.0.0.1] map[serverName:Beijing_VPN serverIP:127.0.0.2]] status:false]
name  is string  testJSON
servers is an array
0 map[serverName:Shanghai_VPN serverIP:127.0.0.1]
1 map[serverIP:127.0.0.2 serverName:Beijing_VPN]
status is of a type i don't know how to handle

 

2.go-simplejson包, https://github.com/bitly/go-simplejson

使用上面的类型断言的方法操作起来其实并不是十分方便,我们可以使用一个名为go-simplejson的包,用来处理未知结构体的JSON

详细内容可见go-simplejson文档学习

举例:

package main 
import(
    "fmt"
    "github.com/bitly/go-simplejson"
    "bytes"
    "log"
)

func main() {
    buf := bytes.NewBuffer([]byte(`{
        "test": {
            "array": [1, "2", 3],
            "arraywithsubs": [
                {"subkeyone": 1},
                {"subkeytwo": 2, "subkeythree": 3}
            ],
            "bignum": 8000000000
        }
    }`))
    js, err := simplejson.NewFromReader(buf)
    if err != nil || js == nil{
        log.Fatal("something wrong when call NewFromReader")
    }
    fmt.Println(js) //&{map[test:map[array:[1 2 3] arraywithsubs:[map[subkeyone:1] map[subkeytwo:2 subkeythree:3]] bignum:8000000000]]}


    arr, err := js.Get("test").Get("array").Array()
    if err != nil || arr == nil{
        log.Fatal("something wrong when call Get and Array")
    }
    fmt.Println(arr) //[1 2 3]
    
    //使用下面的Must类方法就不用判断而err了
    fmt.Println(js.Get("test").Get("array").MustArray()) //[1 2 3]

    fmt.Println(js.Get("test").Get("arraywithsubs").GetIndex(0).MustMap()) //map[subkeyone:1]

    fmt.Println(js.Get("test").Get("bignum").MustInt64()) //8000000000
}

 

2》生成JSON

使用两个函数:

  • func Marshal(v interface{}) ([]byte, error)
  • func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

详细内容可见go标准库的学习-encoding/json

举例:

package main 
import(
    "fmt"
    "encoding/json"
    "os"
)
type Address struct {
    City, State string
}
type Person struct {
    Id        int      `json:"id"` //id作为该JSON字段的key值
    FirstName string    //不设置标签则默认使用FirstName为字段key值
    LastName  string   `json:"-"` //字段被本包忽略,即使有值也不输出
    Age       int      `json:",omitempty"` //含omitempty选项的字段如果为空值会省略,如果存在Age作为该JSON字段的key值
    Height    float32  `json:"height,omitempty"` //含omitempty选项的字段如果为空值会省略,如果存在height作为该JSON字段的key值
    Address         //匿名字段(其标签无效)会被处理为其字段是外层结构体的字段,所以没有Address这个元素,而是直接显示City, State这两个元素
}

func main() {
    v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42}
    v.Address = Address{"Hanga Roa", "Easter Island"}
    output, err := json.MarshalIndent(v, "&", "    ")//每行以&作为前缀,并缩进四个空格
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    os.Stdout.Write(output)
}

返回:

userdeMBP:go-learning user$ go run test.go
{
&    "id": 13,
&    "FirstName": "John",
&    "Age": 42,
&    "City": "Hanga Roa",
&    "State": "Easter Island"
&}

如果使用的是Marshal,则返回:

userdeMBP:go-learning user$ go run test.go
{"id":13,"FirstName":"John","Age":42,"City":"Hanga Roa","State":"Easter Island"}

 Marshal只有在转换成功的时候才会返回数据,因此在转换过程中我们需要注意:

  • Json对象只支持string作为key,所以要编码一个map,必须是map[string]T这种类型(T是Go语言中任意类型)
  • Channel,complex和function是不能够被编码成JSON的
  • 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
  • 指针在编码的时候会输出指针指向的内容,而空指针会输出null

 

3)正则处理

虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。可以使用官方提供的regexp标准包

⚠️:所有的字符都是UTF-8编码的

1》通过正则判断是否匹配

使用regexp包中的三个函数来实现,匹配则返回true,否则返回false:

  • func Match(pattern string, b []byte) (matched bool, err error)
  • func MatchString(pattern string, s string) (matched bool, err error)
  • func MatchReader(pattern string, r io.RuneReader) (matched bool, err error)

上面的三个函数都实现了同一个功能,就是判断pattern是否和输入源匹配,匹配就返回true,如果解析正则出错则返回error。三者的区别在于输入源不同

比如验证一个输入是不是IP地址:

package main 
import(
    "fmt"
    "regexp"
)

func IsIP(ip string) (b bool){
    if m, _ := regexp.MatchString("^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$", ip); !m{
        return false
    }
    return true
}

func main() {
    fmt.Println(IsIP("127.0.0.1")) //true
    fmt.Println(IsIP("127.0.0.1.1")) //false
}

 

2)通过正则获取内容

 Match模式只能用来对字符串的判断,如果想要截取部分字符串、过滤字符串或者提取出符合条件的一批字符串,就应该使用更复杂的方法

1》解析正则表达式的方法有:

  • func Compile(expr string) (*Regexp, error)
  • func CompilePOSIX(expr string) (*Regexp, error)
  • func MustCompile(str string) *Regexp
  • func MustCompilePOSIX(str string) *Regexp

上面的函数用于解析正则表达式是否合法,如果正确,则会返回一个Regexp,然后就能够利用该对象在任意字符串上执行需要的操作

带POSIX后缀的不同点在于其使用POSIX语法,该语法使用最长最左方式搜索,而不带该后缀的方法是采用最左方式搜索(如[a-z]{2,4}这样的正则表达式,应用于"aa09aaa88aaaa"这个文本串时,带POSIX后缀的将返回aaaa,不带后缀的则返回aa)。

前缀有Must的函数表示在解析正则表达式时,如果匹配模式串不满足正确的语法则直接panic,而不加Must前缀的将只是返回错误

2》解析完正则表达式后能够进行的操作有

1〉查找操作——即前缀带有Find的函数

详情可见go标准库的学习-regexp

2)替换操作-即前缀带有Replace的函数

详情可见go标准库的学习-regexp

 举例:

服务端为:

package main 
import(
    "fmt"
    "net/http"
    "log"
    "html/template"
)


func index(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中
}

func login(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    r.ParseForm()
    if r.Method == "GET"{
        html := `<html>
<HEAD>
<title></title>
</HEAD>
<body>


<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
    {{.}}
</form>


</body>
</html>`
        t := template.Must(template.New("test").Parse(html))
        w.Header().Set("Content-Type", "text/html")
        t.Execute(w, nil)
    }
}

func main() {
    http.HandleFunc("/", index)              //设置访问的路由
    http.HandleFunc("/login", login)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
}

客户端

package main 
import(
    "fmt"
    "regexp"
    "net/http"
    "strings"
    "io/ioutil"
)

func main() {
    resp, err := http.Get("http://localhost:9090/login")
    if err != nil{
        fmt.Println("http get err")
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil{
        fmt.Println("http read err")
        return
    }
    src := string(body)
    fmt.Println("--------begin--------")
    fmt.Println(src)

    //将HTML标签都转成小写
    re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllStringFunc(src, strings.ToLower)
    fmt.Println("--------one--------")
    fmt.Println(src)

    //去除head
    re, _ = regexp.Compile("\\<head[\\S\\s]+?\\</head\\>")
    src = re.ReplaceAllString(src, "")
    fmt.Println("--------two--------")
    fmt.Println(src)

    //去除所有尖括号内的HTML代码,并换成换行符
    re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllString(src, "\n")
    fmt.Println("--------three--------")
    fmt.Println(src)   

    //去除连续的换行符
    re, _ = regexp.Compile("\\s{2,}")
    src = re.ReplaceAllString(src, "\n")
    fmt.Println("--------four--------")
    fmt.Println(src)
    //去掉空行
    fmt.Println("--------five--------")
    fmt.Println(strings.TrimSpace(src)) //
}

返回:

userdeMacBook-Pro:go-learning user$ go run test.go
--------begin--------
<html>
<HEAD>
<title></title>
</HEAD>
<body>


<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
    
</form>


</body>
</html>
--------one--------
<html>
<head>
<title></title>
</head>
<body>


<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
    
</form>


</body>
</html>
--------two--------
<html>

<body>


<form action="http://localhost:9090/login" method="post">
    username: <input type="text" name="username">
    password: <input type="text" name="password">
    <input type="submit" value="login">
    
</form>


</body>
</html>
--------three--------









    username: 

    password: 

    

    








--------four--------

username:
password:

--------five--------
username:
password:

 

3)前缀为Expand的函数

举例:

package main 
import(
    "fmt"
    "regexp"

)

func main() {
    src := []byte(`
        call hello alice
        hello bob
        call hello eve
    `)
    pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
    res := []byte{}
    for _, s := range pat.FindAllSubmatchIndex(src, -1){
        res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
    }
    fmt.Println(string(res))
}

返回:

userdeMacBook-Pro:go-learning user$ go run test.go
hello('alice') //
hello('eve')

userdeMacBook-Pro:go-learning user$ 

上面例子使用到的正则表达式语法为

分组:

        (re)           编号的捕获分组
        (?P<name>re)   命名并编号的捕获分组
        (?:re)         不捕获的分组
        (?flags)       设置当前所在分组的标志,不捕获也不匹配
        (?flags:re)    设置re段的标志,不捕获的分组

flags的语法为xyz(设置)、-xyz(清楚)、xy-z(设置xy,清楚z),标志如下:

        I              大小写敏感(默认关闭)
        m              ^和$在匹配文本开始和结尾之外,还可以匹配行首和行尾(默认开启)
        s              让.可以匹配\n(默认关闭)
        U              非贪婪的:交换x*和x*?、x+和x+?……的含义(默认关闭)

因此

  • (?m)表示行首
  • (?P<cmd>\w+)表示将符合\w+(\w== [0-9A-Za-z_])正则表达式的值命名为name,之后再使用函数时可以使用$name来表达

 

4)模版处理—text/template包

MVC设计模式:Model处理数据,View展示结果,Controller控制用户的请求。

在View层的处理都是通过在静态HTML中插入动态语言生成的数据来实现的,模版就是用来获取数据然后渲染数据的。

详情可见go标准库的学习-text/template

 

5)字符串处理

1》字符串操作——常用函数——strings包

func Contains

func Contains(s, substr string) bool

判断字符串s是否包含子串substr。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Println(strings.Contains("seafood", "foo")) //true
    fmt.Println(strings.Contains("seafood", "bar")) //false
    fmt.Println(strings.Contains("seafood", "")) //true
    fmt.Println(strings.Contains("", "")) //true
}

 

func Index

func Index(s, sep string) int

子串sep在字符串s中第一次出现的位置,不存在则返回-1。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Println(strings.Index("chicken", "ken")) //4
    fmt.Println(strings.Index("chicken", "dmr")) //-1
}

 

func Join

func Join(a []string, sep string) string

将一系列字符串连接为一个字符串,之间用sep来分隔。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    s := []string{"foo", "bar", "baz"}
    fmt.Println(strings.Join(s, ", "))//foo, bar, baz
}

 

func Repeat

func Repeat(s string, count int) string

返回count个s串联的字符串。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Println("ba" + strings.Repeat("na", 2)) //banana
}

 

func Replace

func Replace(s, old, new string, n int) string

返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) //oinky oinky oink
    fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) //moo moo moo
}

 

func Split

func Split(s, sep string) []string

用去掉s中出现的sep的方式进行分割,会分割到结尾,并返回生成的所有片段组成的切片(每一个sep都会进行一次切割,即使两个sep相邻,也会进行两次切割)。如果sep为空字符,Split会将s切分成每一个unicode码值一个字符串。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Printf("%q\n", strings.Split("a,b,c", ",")) //["a" "b" "c"]
    fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) //["" "man " "plan " "canal panama"]
    fmt.Printf("%q\n", strings.Split(" xyz ", "")) //[" " "x" "y" "z" " "]
    fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) //[""]
}

 

 

func Trim

func Trim(s string, cutset string) string

返回将s前后端所有cutset包含的utf-8码值都去掉的字符串。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Printf("[%q]\n", strings.Trim(" !!! Achtung! Achtung! !!! ", "! ")) //["Achtung! Achtung"]
}

 

func Fields

func Fields(s string) []string

返回将字符串按照空白(unicode.IsSpace确定,可以是一到多个连续的空白字符)分割的多个字符串。如果字符串全部是空白或者是空字符串的话,会返回空切片。

举例:

package main 
import(
    "fmt"
    "strings"

)

func main() {
    fmt.Printf("Fields are: %q\n", strings.Fields("  foo bar  baz   ")) //Fields are: ["foo" "bar" "baz"]
}

 更多详情可见go标准库的学习-strings-字符串操作

2)字符串转换——strconv包

strconv包实现了基本数据类型和其字符串表示的相互转换。

1)append系列

将值添加到现有的字节数组中

func AppendBool

func AppendBool(dst []byte, b bool) []byte

等价于append(dst, FormatBool(b)...)

func AppendInt

func AppendInt(dst []byte, i int64, base int) []byte

等价于append(dst, FormatInt(I, base)...)

func AppendUint

func AppendUint(dst []byte, i uint64, base int) []byte

等价于append(dst, FormatUint(I, base)...)

func AppendFloat

func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte

等价于append(dst, FormatFloat(f, fmt, prec, bitSize)...)

func AppendQuote

func AppendQuote(dst []byte, s string) []byte

等价于append(dst, Quote(s)...)

func AppendQuoteToASCII

func AppendQuoteToASCII(dst []byte, s string) []byte

等价于append(dst, QuoteToASCII(s)...)

func AppendQuoteRune

func AppendQuoteRune(dst []byte, r rune) []byte

等价于append(dst, QuoteRune(r)...)

func AppendQuoteRuneToASCII

func AppendQuoteRuneToASCII(dst []byte, r rune) []byte

等价于append(dst, QuoteRuneToASCII(r)...)

package main 
import(
    "fmt"
    "strconv"

)

func main() {
    str := make([]byte, 0, 100)
    str = strconv.AppendInt(str, 4567, 10)
    str = strconv.AppendBool(str, false)
    str = strconv.AppendQuote(str, "abcdefg")
    str = strconv.AppendQuoteRune(str, '')
    fmt.Println(string(str)) //4567false"abcdefg"'单'

}

 

2)Format系列

将其他类型值转换为字符串

func FormatBool

func FormatBool(b bool) string

根据b的值返回"true"或"false"。

func FormatInt

func FormatInt(i int64, base int) string

返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母'a'到'z'表示大于10的数字。

func FormatUint

func FormatUint(i uint64, base int) string

是FormatInt的无符号整数版本。

func FormatFloat

func FormatFloat(f float64, fmt byte, prec, bitSize int) string

函数将浮点数表示为字符串并返回。

bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。

fmt表示格式:'f'(-ddd.dddd)、'b'(-ddddp±ddd,指数为二进制)、'e'(-d.dddde±dd,十进制指数)、'E'(-d.ddddE±dd,十进制指数)、'g'(指数很大时用'e'格式,否则'f'格式)、'G'(指数很大时用'E'格式,否则'f'格式)。

prec控制精度(排除指数部分):对'f'、'e'、'E',它表示小数点后的数字个数;对'g'、'G',它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

举例:
package main 
import(
    "fmt"
    "strconv"

)

func main() {
    a := strconv.FormatBool(false)
    //'g'表示格式(指数很大时用'e'格式-d.dddde±dd,否则'f'格式-ddd.dddd)
    //对于'g',后面的12表示控制总的数字个数,控制精度
    //64表示来源类型为float64
    b := strconv.FormatFloat(123.23, 'g', 12, 64) 
    e := strconv.FormatFloat(123.23, 'g', 4, 64) 
    c := strconv.FormatInt(1234, 10) //10表示10进制
    d := strconv.FormatUint(12345, 10)
    fmt.Println(a, b, e, c, d) //false 123.23 123.2 1234 12345

}

 

3)Parse系列

把字符串转换为其他类型

func ParseBool

func ParseBool(str string) (value bool, err error)

返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。

func ParseInt

func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

base指定进制(2到36),如果base为0,则会从字符串前置判断,"0x"是16进制,"0"是8进制,否则是10进制;

bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

func ParseUint

func ParseUint(s string, base int, bitSize int) (n uint64, err error)

ParseUint类似ParseInt但不接受正负号,用于无符号整型。

func ParseFloat

func ParseFloat(s string, bitSize int) (f float64, err error)

解析一个表示浮点数的字符串并返回其值。

如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;返回值err是*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inf,err.Error= ErrRange。

举例:
package main 
import(
    "fmt"
    "strconv"

)

func main() {
    a, err := strconv.ParseBool("false")
    if err != nil{
        fmt.Println(err)
    }
    //64表示返回类型为float64
    b, err := strconv.ParseFloat("123.23", 64) 
    if err != nil{
        fmt.Println(err)
    }
    //10表示10进制,64表示返回int64
    c, err := strconv.ParseInt("1234", 10, 64) 
    if err != nil{
        fmt.Println(err)
    }
    d, err := strconv.ParseUint("12345", 10, 64)
    if err != nil{
        fmt.Println(err)
    }
    fmt.Println(a, b, c, d) //false 123.23 1234 12345

}

 

7.web服务

web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息

web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统上,可以与其他Window的asp.net程序交互。同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信

目前有以下几种主流的web服务:REST(更简洁),SOAP,XML-RPC

什么是REST?可见https://blog.csdn.net/qq_21383435/article/details/80032375

 

1)Socket编程

常见socket(套接字)分为两种:

  • 流式socket(SOCK STREAM)
  • 数据报Socket(SOCK DGRAM)

流式是一种面向连接的socket,针对面向连接的TCP服务应用;数据报式socket是一种无连接的socket,对应于无连接的UDP服务应用

进程间通过socket进行通信需要解决如何唯一标识一个进程的问题:

在本地可以通过进程PID来唯一标识一个进程,但是这在网络中是行不通的。TCP/IP协议族已经解决了这个问题

通过网络层的IP地址可以为宜标识网络中的主机,而传输层的"协议TCP/UDP+端口"能够唯一标识主机中的进程。这样讲两者结合起来,利用三元组(IP地址,协议,端口)就可以唯一表示网络上的进程了,这样进程间就能够进行通信了

Go的net包中定义了很多类型、函数和方法用来网络编程

1》其中IP的定义为:

type IP

type IP []byte

常用方法有:

func ParseIP

func ParseIP(s string) IP

ParseIP将s解析为IP地址,并返回该地址。如果s不是合法的IP地址文本表示,ParseIP会返回nil。

字符串可以是小数点分隔的IPv4格式(如"74.125.19.99")或IPv6格式(如"2001:4860:0:2001::68")格式。

举例:

package main 
import(
    "fmt"
    "os"
    "net"
)

func main() {
    if len(os.Args) != 2{
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]
    addr := net.ParseIP(name)
    if addr == nil {
        fmt.Println("Invalid address")
    }else{
        fmt.Println("the address is", addr.String())
    }
    os.Exit(0)
}

返回:

userdeMacBook-Pro:go-learning user$ go run test.go
Usage: /var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build258331112/b001/exe/test ip-addr
exit status 1
userdeMacBook-Pro:go-learning user$ go run test.go 127.0.0.1
the address is 127.0.0.1

 

2》TCP Socket —— net包中的TCPConn

当我们知道如何通过网络端口访问一个服务后:

  • 作为客户端,我们可以通过向远端某台机器的某个网络端口发送一个请求,然后在机器的此端口上监听服务反馈的信息
  • 作为服务端,需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息

Go的net包中有一个类型TCPConn,这个类型能够用来作为客户端和服务器交互的通道,里面主要使用的是下面的两个函数:

  • func (c *TCPConn) Read(b []byte) (int, error)
  • func (c *TCPConn) Write(b []byte) (int, error)

 还需要知道TCPAddr类型:

type TCPAddr

type TCPAddr struct {
    IP   IP
    Port int
    Zone string // IPv6范围寻址域
}

TCPAddr代表一个TCP终端地址。

使用的函数有:

  • func ResolveTCPAddr(net, addr string) (*TCPAddr, error)

参数addr表示域名或IP地址,如“www.baidu.com:80”或“127.0.0.1:22”。格式为"host:port"或"[ipv6-host%zone]:port",解析得到网络名和端口名;

net参数必须是"tcp"、"tcp4"或"tcp6",表示(IPv4\IPv6任意一个)、(IPv4-only)或者(IPv6-only)。

1>TCP客户端

首先使用下面的函数来建立一个TCP连接,并返回一个TCPConn类型的对象,当连接建立时服务器端也会创建一个同类型的对象,此时客户端和服务端通过各自拥有的TCPConn对象来进行数据交换:

  • func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)

参数net必须是"tcp"、"tcp4"、"tcp6",表示(IPv4\IPv6任意一个)、(IPv4-only)或者(IPv6-only);

laddr表示本机地址,一般为nil.如果laddr不是nil,将使用它作为本地地址,即客户端,否则自动选择一个本地地址;

raddr表示远程的服务地址,即服务端

 

一般来说,客户端通过TCPConn对象将请求信息发送到服务端,然后等待读取服务端响应的信息。服务端读取并解析来自客户端的请求,并返回应答信息。这个连接只有当任一端关闭后连接才会失效,不然连接可以一直使用

举例,模拟一个基于HTTP协议的客户端请求去连接一个web服务端

首先是实现一个客户端,代码如下:

package main 
import(
    "fmt"
    "net"
    "io/ioutil"
    "os"
)

func checkError(index int, err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error())
        os.Exit(1)
    }
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(1, err)

    //创建一个TCP连接conn
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(2, err)

    //通过conn来发送请求信息
    _, err = conn.Write([]byte("HEAD / HTTP/1.0 \r\n\r\n"))
    checkError(3, err)

    //从conn中读取服务端返回的全部的文本
    result, err := ioutil.ReadAll(conn)
    checkError(4, err)

    fmt.Println(string(result))
    os.Exit(0)
}

 

2>TCP服务端

然后是编写服务端,实现简单的时间同步服务,监听7777端口:

在服务端需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求,需要使用的函数有:

  • func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
  • func (l *TCPListener) Accept() (Conn, error)

详情可见go标准库的学习-net

package main 
import(
    "fmt"
    "net"
    "os"
    "time"
)

func checkError(err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for i := 0; i < 4; i++{
        conn, err := listener.Accept()
        if err != nil{
            continue
        }
        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()
    }
}

客户端访问服务器后会出错:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:7777
Fatal error : read tcp 127.0.0.1:51349->127.0.0.1:7777: read: connection reset by peerexit status 1

这个错误是因为使用了ioutil.ReadAll(conn),原因是什么不清楚,推荐还是使用conn.Read()方法,即客户端改成:

package main 
import(
    "fmt"
    "net"
    "os"
)

func checkError(index int, err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error())
        os.Exit(1)
    }
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(1, err)

    //创建一个TCP连接conn
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(2, err)

    //通过conn来发送请求信息
    _, err = conn.Write([]byte("HEAD / HTTP/1.0 \r\n\r\n"))
    checkError(3, err)


    //从conn中读取服务端返回的全部的文本
    rsp := make([]byte, 64)
    n, err := conn.Read(rsp)
    checkError(4, err)
    fmt.Printf("receive %d bytes in response : %q\n", n, rsp[:n])

    os.Exit(0)
}

上面的服务器跑起来后,它会一直在那里等待,直到有新的客户端请求到达并同意接收Accept()该请求,这时候服务端就会反馈给客户端当前的时间信息

在该代码的for循环中当有错误发生时,直接continue而不是退出,是因为在服务端跑代码的时候,当有错误发生的情况下最好是由服务器记录错误,然后当前连接的客户端直接报错而退出, 从而不会影响到当前服务端运行的整个服务

上面的代码的缺点在于执行的时候是单任务的,不能同时接收多个请求。为了支持并发使用了goroutine机制,因此服务端改成:

package main 
import(
    "fmt"
    "net"
    "os"
    "time"
)

func checkError(err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for{ 
        conn, err := listener.Accept()
        if err != nil{
            continue
        }
        go handleerClient(conn)
    }
}

func handleerClient(conn net.Conn){
    defer conn.Close() 
    daytime := time.Now().String()
    conn.Write([]byte(daytime))
}

上面通过将业务处理分离到函数handleerClient,这样就能够实现多并发执行了

这样运行起服务端监听等待着客户端的请求,然后再运行客户端能够返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:1200
receive 51 bytes in response : "2019-02-27 15:25:02.113373 +0800 CST m=+5.168778770"

 

3>控制TCP连接函数有:

  • func (c *TCPConn) SetKeepAlive(keepalive bool) error
  • func (c *TCPConn) SetTimeOut(nsec int64) error

SetTimeOut函数用来设置连接的超时时间,客户端和服务端都适用,当超过设置的时间该连接就会失效

SetKeepAlive函数用来设置客户端是否和服务端一直保持着连接,即使没有任何的数据发送

详情可见go标准库的学习-net

 

3》UDP Socket —— net包中的UDPConn

和上面的TCPConn的操作是相似的,不同之处是服务端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数,使用下面的几个函数:

  • func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
  • func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
  • func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)
  • func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
  • func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
  • func (c *UDPConn) Read(b []byte) (int, error)
  • func (c *UDPConn) Write(b []byte) (int, error)

客户端:

package main 
import(
    "fmt"
    "net"
    "os"
)

func checkError(index int, err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error())
        os.Exit(1)
    }
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    udpAddr, err := net.ResolveUDPAddr("udp", service)
    checkError(1, err)

    //创建一个TCP连接conn
    conn, err := net.DialUDP("udp", nil, udpAddr)
    defer conn.Close()
    checkError(2, err)

    //通过conn来发送请求信息
    _, err = conn.Write([]byte("anything"))
    checkError(3, err)

    //从conn中读取服务端返回的全部的文本
    var rsp [512]byte
    n, err := conn.Read(rsp[0:])
    checkError(4, err)

    fmt.Printf("receive %d bytes in response : %q\n", n, rsp[:n])
    os.Exit(0)
}

 

服务端

package main 
import(
    "fmt"
    "net"
    "os"
    "time"
)

func checkError(err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

func main() {
    service := ":11200"
    udpAddr, err := net.ResolveUDPAddr("udp", service)
    checkError(err)

    conn, err := net.ListenUDP("udp", udpAddr)
    defer conn.Close()
    checkError(err)

    for{ 
        go handlerClient(conn)
    }
}

func handlerClient(conn *net.UDPConn){
    var rsp [512]byte
    _, addr, err := conn.ReadFromUDP(rsp[0:])
    if err != nil{
        return
    }
    daytime := time.Now().String()
    conn.WriteToUDP([]byte(daytime), addr)
}

 

客户端返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:11200
receive 51 bytes in response : "2019-02-27 16:25:46.905443 +0800 CST m=+2.197257345"

 

2)WebSocket——实现即时更新数据,浏览器与服务器能进行全双工通信

参考http://www.runoob.com/html/html5-websocket.html

socket和websocket的区别():

Socket 其实并不是一个协议,是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。当两台主机通信时,让 Socket 去组织数据,以符合指定的协议。TCP 连接则更依靠于底层的 IP 协议,IP 协议的连接则依赖于链路层等更低层次。

WebSocket 则是一个典型的应用层协议。

总的来说:Socket 是传输控制层协议,WebSocket 是应用层协议

 

Websocket 使用 ws 或 wss 的统一资源标志符,类似于 HTTPS,其中 wss 表示在 TLS 之上的 Websocket。如:

ws://example.com/wsapi
wss://secure.example.com/

Websocket 使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,Websocket 协议使用 80 端口;运行在 TLS 之上时,默认使用 443 端口。

一个典型的Websocket握手请求如下:

客户端请求

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

服务器回应

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
  •  Connection 必须设置 Upgrade,表示客户端希望连接升级。
  •  Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
  •  Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
  •  Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
  •  Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。
  •  其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。

websocket协议的交互流程:

客户端首先发起一个Http请求到服务端,请求的特殊之处,在于在请求里面带了一个upgrade的字段,告诉服务端,我想生成一个websocket的协议,服务端收到请求后,会给客户端一个握手的确认,返回一个switching, 意思允许客户端向websocket协议转换,完成这个协商之后,客户端与服务端之间的底层TCP协议是没有中断的,接下来,客户端可以向服务端发起一个基于websocket协议的消息,服务端也可以主动向客户端发起websocket协议的消息,websocket协议里面通讯的单位就叫message。

 

Go语言标准包中没有提供对WebSocket的支持,但是在官方维护的go.net子包中有对这个的支持,安装该包:

go get github.com/gorilla/websocket

客户端通过WebSocket函数建立了一个与服务器的连接sock。当握手成功后,会触发WebSocket对象的onopen事件,告诉客户端连接已经建立成功。在客户端中一共绑定了四个事件:

  • onopen:建立连接后会触发该事件
  • onmessage:收到消息后会触发该事件
  • onerror:发生错误时会触发该事件
  • onclose:关闭连接时会触发该事件

 服务端代码:

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

var upgrader = websocket.Upgrader{} // use default options

func echo(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

func home(w http.ResponseWriter, r *http.Request) {
    homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}

func main() {
    flag.Parse()
    log.SetFlags(0)
    http.HandleFunc("/echo", echo)
    http.HandleFunc("/", home)
    log.Fatal(http.ListenAndServe(*addr, nil))
}

var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>  
window.addEventListener("load", function(evt) {
    var output = document.getElementById("output");
    var input = document.getElementById("input");
    var ws;
    var print = function(message) {
        var d = document.createElement("div");
        d.innerHTML = message;
        output.appendChild(d);
    };
    document.getElementById("open").onclick = function(evt) {
        if (ws) {
            return false;
        }
        ws = new WebSocket("{{.}}");
        ws.onopen = function(evt) {
            print("OPEN");
        }
        ws.onclose = function(evt) {
            print("CLOSE");
            ws = null;
        }
        ws.onmessage = function(evt) {
            print("RESPONSE: " + evt.data);
        }
        ws.onerror = function(evt) {
            print("ERROR: " + evt.data);
        }
        return false;
    };
    document.getElementById("send").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        print("SEND: " + input.value);
        ws.send(input.value);
        return false;
    };
    document.getElementById("close").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        ws.close();
        return false;
    };
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server, 
"Send" to send a message to the server and "Close" to close the connection. 
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))

然后在浏览器中调用127.0.0.1:8080:

点击open将创建连接,send将发送数据,close将关闭连接。然后就会相应地在网站的右边返回输出:

服务端的输出为:

recv: Hello world!
read: websocket: close 1005 (no status)

 

当然,也可以编写一个客户端来与服务端互相发送时间信息:

package main

import (
    "flag"
    "log"
    "net/url"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

func main() {
    flag.Parse()
    log.SetFlags(0)

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
    log.Printf("connecting to %s", u.String())

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            _, message, err := c.ReadMessage()
            if err != nil {
                log.Println("read:", err)
                return
            }
            log.Printf("recv: %s", message)
        }
    }()

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
            if err != nil {
                log.Println("write:", err)
                return
            }
        case <-interrupt:
            log.Println("interrupt")

            // Cleanly close the connection by sending a close message and then
            // waiting (with timeout) for the server to close the connection.
            err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
            if err != nil {
                log.Println("write close:", err)
                return
            }
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

客户端返回:

userdeMBP:go-learning user$ go run test.go
connecting to ws://localhost:8080/echo
recv: 2019-02-27 17:29:48.742574 +0800 CST m=+1.004759822
recv: 2019-02-27 17:29:49.740974 +0800 CST m=+2.003161426
recv: 2019-02-27 17:29:50.744928 +0800 CST m=+3.007116994

服务端返回:

userdeMacBook-Pro:go-learning user$ go run test1.go
recv: 2019-02-27 17:29:48.742574 +0800 CST m=+1.004759822
recv: 2019-02-27 17:29:49.740974 +0800 CST m=+2.003161426
recv: 2019-02-27 17:29:50.744928 +0800 CST m=+3.007116994
recv: 2019-02-27 17:29:51.745137 +0800 CST m=+4.007328841
...

这个代码之后有时间并且弄完go websocket包——gorilla/websocket学习-没弄-最好学学后再仔细看看!!!!!!

 

 

 3)REST

RESTful是一种互联网软件架构:

  • 每一个URI代表一种资源
  • 客户端和服务器之间,传递这种资源的某种表现层
  • 客户端通过四个HTTP动词,对服务器资源进行操作,实现“表现层状态转化”。GET用来获取资源,POST用来新建资源(也可以用来更新资源),PUT用来更新资源,DELETE用来删除资源

web应用要满足REST的原则:

最重要的是客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能

另一个是系统分层,即组件无法了解除了与它直接交互的层次之外的组建,用于限制整个系统的复杂性,从而促进底层的独立性

下面的代码使用了库:

go get github.com/drone/routes

 举例:

服务端:

package main

import (
    "fmt"
    "net/http"
    "github.com/drone/routes"
)

func getUser(w http.ResponseWriter, r *http.Request){
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are get user %s\n", uid)
}

func modifyUser(w http.ResponseWriter, r *http.Request){
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are modify user %s\n", uid)
}

func deleteUser(w http.ResponseWriter, r *http.Request){
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are delete user %s\n", uid)
}

func addUser(w http.ResponseWriter, r *http.Request){
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are add user %s\n", uid)
}
func main() {
    mux := routes.New()
    //同样的URI,当时使用的是不同的方法进行访问则调用的是不同的函数
    mux.Get("/user/:uid", getUser)
    mux.Post("/user/:uid", modifyUser)
    mux.Del("/user/:uid", deleteUser)
    mux.Put("/user/", addUser)
    http.Handle("/", mux)
    http.ListenAndServe(":8088", nil)
}

使用浏览器访问默认使用的是GET方法:

 

然后是使用代码访问,客户端:

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    req, err := http.NewRequest("GET","http://127.0.0.1:8088/user/:uid", nil)
    if err != nil { 
        fmt.Println(err)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Println(err)
    }
    defer resp.Body.Close()
    b, _ := ioutil.ReadAll(resp.Body)
    fmt.Printf("info : %q\n", b)
}

首先使用的是GET方法,然后使用POST、DELETE,返回:

userdeMBP:go-learning user$ go run test.go
info : "you are get user :uid\n"
userdeMBP:go-learning user$ go run test.go
info : "you are modify user :uid\n"
userdeMBP:go-learning user$ go run test.go
info : "you are delete user :uid\n"

然后调用PUT方式是:

req, err := http.NewRequest("PUT","http://127.0.0.1:8088/user/", nil)

返回:

userdeMBP:go-learning user$ go run test.go
info : "you are add user \n"

根据上面的代码可知,其实REST就是根据不同的method访问同一个资源的时候实现不同的逻辑处理!!!

 

4)RPC

上面的socket和HTTP采用的是类似“信息交换”模式,即客户端发送一条信息到服务端,然后服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用常规的函数调用的方式来完成想要的功能。

RPC(Remote Procedure Call Protocol)就是想实现函数调用模式的网络化,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传给服务端,服务端解包到处理过程中执行,然后执行结果返回给客户端

运行时一次客户机对服务器的RPC调用步骤有:

  • 调用客户端句柄,执行传送参数
  • 调用本地系统内核发送网络信息
  • 消息传送到远程主机
  • 服务器句柄得到消息并取得参数
  • 执行远程过程
  • 执行的过程将结果返回服务器句柄
  • 服务器句柄返回结果,调用远程系统内核
  • 消息传回本地主机
  • 客户句柄由内核接收消息
  • 客户接收句柄返回的数据

Go标准包中已经提供了对RPC的支持,支持三个级别的RPC:TCP、HTTP、JSONRPC,下面将一一说明

Go的RPC包与传统的RPC系统不同,他只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码

Go RPC的函数要满足下面的条件才能够被远程调用,不然会被忽略:

  • 函数必须是导出的,即首字母为大写
  • 必须有两个导出类型的参数
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个error类型返回值。方法的返回值,如果非nil,将被作为字符串回传,在客户端看来就和errors.New创建的一样。如果返回了错误,回复的参数将不会被发送给客户端。

举个例子,正确的RPC函数格式为:

func (t *T) MethidName(argType T1, replyType *T2) error

T、T1和T2类型都必须能被encoding/gob包编解码

任何RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据

1》HTTP RPC

利用HTTP的好处是可以直接复用net/http中的一些函数,下面举例说明:

服务端:

package main

import (
    "fmt"
    "net/http"
    "net/rpc"
    "errors"
)

type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error{
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error{
    if args.B == 0{
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {
    arith := new(Arith)
    rpc.Register(arith)
    rpc.HandleHTTP()

    err := http.ListenAndServe(":1234", nil)
    if err != nil{
        fmt.Println(err.Error())
    }
}

客户端:

package main

import (
    "fmt"
    "net/rpc"
    "log"
    "os"
)
type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2{
        fmt.Println("Usage: ", os.Args[0], "server")
        os.Exit(1)
    }
    serverAddress := os.Args[1]

    client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
    if err != nil{
        log.Fatal("dialing : ", err)
    }

    //Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)   
}

客户端返回:

userdeMBP:go-learning user$ go run test.go
Usage:  /var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build438875911/b001/exe/test server
exit status 1
userdeMBP:go-learning user$ go run test.go 127.0.0.1
Arith: 17*8 = 136 
Arith: 17/8 = 2 remainder 1

 

2》TCP RPC

服务端:

package main

import (
    "fmt"
    "net"
    "net/rpc"
    "errors"
    "os"
)

type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error{
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error{
    if args.B == 0{
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {
    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    if err != nil{
        fmt.Println(err.Error())
        os.Exit(1)
    }

    listener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil{
        fmt.Println(err.Error())
        os.Exit(1)
    }
    for{
        conn, err := listener.Accept()
        if err != nil{
            continue
        }
        rpc.ServeConn(conn)
    }    
}

客户端:

package main

import (
    "fmt"
    "net/rpc"
    "log"
    "os"
)
type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2{
        fmt.Println("Usage: ", os.Args[0], "server:port")
        os.Exit(1)
    }
    service := os.Args[1]

    client, err := rpc.Dial("tcp", service)
    if err != nil{
        log.Fatal("dialing : ", err)
    }

    //Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)   
}

客户端返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1
2019/02/28 11:16:37 dialing : dial tcp: address 127.0.0.1: missing port in address
exit status 1
userdeMBP:go-learning user$ go run test.go 127.0.0.1:1234
Arith: 17*8 = 136 
Arith: 17/8 = 2 remainder 1

其代码与HTTP的代码的区别就是一个是DialHTTP,一个是Dial(tcp)

 

3》JSON RPC

服务端:

package main

import (
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "errors"
    "os"
)

type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error{
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error{
    if args.B == 0{
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {
    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")//jsonrpc是基于TCP协议的,现在他还不支持http协议
    if err != nil{
        fmt.Println(err.Error())
        os.Exit(1)
    }

    listener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil{
        fmt.Println(err.Error())
        os.Exit(1)
    }
    for{
        conn, err := listener.Accept()
        if err != nil{
            continue
        }
        jsonrpc.ServeConn(conn)
    }    
}

客户端:

package main

import (
    "fmt"
    "net/rpc/jsonrpc"
    "log"
    "os"
)
type Args struct{
    A, B int
}

type Quotient struct{
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2{
        fmt.Println("Usage: ", os.Args[0], "server:port")
        os.Exit(1)
    }
    service := os.Args[1]

    client, err := jsonrpc.Dial("tcp", service)
    if err != nil{
        log.Fatal("dialing : ", err)
    }

    //Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil{
        log.Fatal("arith error : ", err)
    }
    fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)   
}

客户端返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:1234
Arith: 17*8 = 136 
Arith: 17/8 = 2 remainder 1

 

这一节介绍了四种主要的网络应用开发方式:socket编程(云计算方向)、WebSocket(html5,服务器可以主动地push消息,以简便以前Ajax轮询的模式)、REST(开发网络应用API)、RPC

 

8.安全与加密

1)预防CSRF攻击

CSRF(跨站请求伪造),攻击者通过在授权用户访问的页面中包含链接或者脚本的方式工作。

要完成一次CSRF攻击,受害者必需满足下面饿两个条件:

  • 登录受信任网站,并在本地生成cookie
  • 在不退出A的情况下,访问危险网站B

比如当用户登录网络银行去查看其存款余额并没有退出时,这时候他的QQ好友发来了一个链接,这个链接可能是攻击者编写了一个在网络银行进行取款的form提交的链接,这样当用户点击该链接后,攻击者就能够得到其网络银行的cookie和该form提交一起实现将用户账户中的资金转移到攻击者的账户中

因此预防的思想主要从两方面入手:

  • 正确使用GET、POST和Cookie
  • 在非GET请求中增加伪随机数

其实主要是在第二步,就像我们之前表单小节“如何防止表单多次递交”中说到的添加伪随机数的操作

因为攻击者编写的虚假表单生成的伪随机数是不合法的,这样即使他通过XSS获得了用户的cookie,他发送的表单也不会被接受

 

2)确保输入过滤

这部分与表单小节中的“对表单的输入进行验证”内容是很类似的

过滤数据分成三步:

  • 识别数据,搞清楚需要过滤的数据来自哪里
  • 过滤数据,弄明白我们需要什么样的数据。可以使用strconv、string、regexp包来过滤数据
  • 区分已过滤数据及被污染数据,如果存在攻击数据,就保证过滤后可以让我们使用更安全的数据

 

3)XSS攻击

  • 防御方式:

过滤特殊字符。可以使用HTML的过滤函数,如text/template包下面的HTMLEscapeString、JSEscapeString等函数

  • 使用HTTP头指定类型
w.Header().Set("Content-Type", "text/javascript")

这样就可以让浏览器解析javascript代码,而不会是html输出

 

4)避免SQL注入

防治SQL注入方法:

  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度减少注入攻击对数据库的危害
  • 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型,例如使用regexp包进行一些匹配处理,或者使用strconv包对字符串转化成其他基本类型的数据进行判断
  • 对进入数据库的特殊字符(‘“\<&*;等)进行转义处理,或编码转换。Go的text/template包里面的HTMLEscapeString函数可以对字符串进行转义处理
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句。例如使用database/sql里面的查询函数Prepare和Query,或者Exec(query string,args ...interface{})
  • 在应用开发之前建议使用专业的SQL注入检测工具进行检测,以及时修补被发现的SQL注入漏洞。如sqlmap、SQLninja等
  • 避免网站打印出SQL错误信息,比如类型错误,字段不匹配等,把代码里的SQL语句暴露出来,以防止攻击者利用这些错误信息进行SQL注入

 

5)存储密码

1》普通方案

现在用得最多的密码存储方案是将明文密码做单向哈希后再存储。常用的有SHA-256、SHA-1、MD5

例子可见本博客

go标准库的学习-crypto/sha1

go标准库的学习-crypto/sha256

go标准库的学习-crypto/md5

2》进阶方案

因为攻击者能够用rainbow table来解析哈希后的密码,因此进行“加盐”——salt来进阶

即先将用户输入的密码进行一次MD5等算法加密;然后将得到的MD5值前后加上一些只有管理员自己知道的随机串(salt),在进行一次MD5机密

这个随机串(salt)可以包含某些固定的串,也可以包含用户名

3》专家方案——scrypt方案

因为并行计算能力的提升,使用多个rainbow table来攻击变得可行

新方案即:通过故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的rainbow table

该方案的特点为:算法中有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立rainbow table越困难,以至于不可继续

scrypt方案

http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt

dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)

通过上面的方法可以获得唯一的相应的密码值,这是目前最难破解的

可见本博客的keystore密钥文件使用的算法-PBKDF2WithHmacSHA1 和Scrypt

 

6)加密和解密数据

1》base64加解密

详情可见go标准库的学习-encoding/base64

举例:

package main 
import(
    "fmt"
    "encoding/base64"
)

func main() { 
    msg := "Hello,world" //11个字符应该装成15个base64编码的字符
    encoded := base64.StdEncoding.EncodeToString([]byte(msg))
    fmt.Println(encoded) //SGVsbG8sd29ybGQ=,后面的=是作填充用的
    decoded, err := base64.StdEncoding.DecodeString(encoded)
    if err != nil {
        fmt.Println("decode error:", err)
        return
    }
    fmt.Println(string(decoded))//Hello,world
}

 

2》高级加解密

使用的是下面的两个标准包:

  • crypto/aes包:又称Rijndael加密法
  • crypto/des包:对称加密算法(老了,不建议使用)

详情可见:

go标准库的学习-crypto/aes

go标准库的学习-crypto/des

 

7) 国际化和本地化

国际化相关的包https://github.com/astaxie/go-i18n

 

8)错误处理,调试和测试

1》错误处理

Go中定义了一个error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。如果出现错误,就会使用log.Fatal(err)来输出错误信息

    if err != nil {
        log.Fatal(err)
    }

 

error类型

error类型是一个内置接口类型,定义为:

type error struct{
    Error() string
}

我们在很多内部包中使用到的error是errors包下面的私有结构errorString:

//errors包中实现了error接口的结构体errorString
type errorString struct{
    s string
}

func (e *errorString) Error() string{
    return e.s
}

//当调用errors.New()函数是其实就是返回了一个实现了error接口的errorString对象
//在这里及时返回的是自定义的error类型,返回值也应该设置为error类型
func New(text string) error{ return &errorString{text} }

举例:

package main 
import(
    "errors"
    "fmt"
)
func main() {
    const name, id = "coco", 17
    err := errors.New(fmt.Sprintf("user %q (id %d )not found", name, id))
    if err != nil{
        fmt.Println(err)
    }
}

详情可见go标准库的学习-errors

 

自定义Error

举例:

package main

import (
    "fmt"
)

//当除数为0时报错
type divdError struct{
    err string //错误描述
    dividend float64 //被除数的值
}

// 虽然这里只返回了err string的信息,但是可以使用类型断言来获得divdError的dividend
func (e *divdError) Error() string{
    return fmt.Sprintf("divdError is %s", e.err)

}

//要注意,函数返回自定义错误时,返回值也应该设置为error,而非自定义类型,也不应该预声明自定义错误类型的变量
//如果返回值设置为*divdError,则可能导致上层调用者err != nil的判断永远为true
//预声明即var err *divdError
func divide(dividend, divisor float64) (float64, error) {
    if divisor == 0{
        return 0, &divdError{"divisor is 0 ", dividend}
    }
    return dividend/divisor, nil
}

func main() {
    divisor := 0.0
    result, err := divide(25.0, divisor)
    if err != nil{
        if err, ok := err.(*divdError);ok{ //使用类型断言来获得divdError的dividend
            fmt.Printf("dividend is %0.2f, divdError is %s\n", err.dividend, err.err)
            return
        }
        fmt.Println(err)
    }
    fmt.Printf("divide result is %0.2f\n", result)
}

 

错误处理:

1.错误代码的冗余——通过检测函数来减少类似的代码

func init(){
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request){
    ...
    if err := datastore.Get(c, key, record); err != nil{ //获取数据
        http.Error(w, err.Error(), 500)
    }
    if err := viewTemplate.Execute(w, record); err != nil{//模版展示
        http.Error(w, err.Error(), 500)
    }    
}

上面的代码在获取数据和模版展示调用时都有检测错误,调用了统一的处理函数http.Error,返回给客户端500错误码,并显示相应的错误信息。但是当越来越多的HandleFunc加入后,这样的错误处理逻辑代码就会开始变得冗余

更改后代码,通过自定义路由器来缩减代码:

详情可见go标准库的学习-net/http

 

func init(){
    http.HandleFunc("/view", appHandler(viewRecord)) //将viewRecord类型转换为appHandler
}

type appHandler func(w http.ResponseWriter, r *http.Request) error

//当访问/view路径时,就会自动调用appHandler的ServeHTTP函数,这样就会对返回的error进行错误处理了
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
    if err := fn(w, r); err != nil{
        http.Error(w, err.Error(), 500)
    }
}

func viewRecord(w http.ResponseWriter, r *http.Request) error{
    ...
    if err := datastore.Get(c, key, record); err != nil{
        return err
    }
    return viewTemplate.Execute(w, record)
}

 

 

2》使用GDB调试

Go只要修改不需要编译就可以直接输出,可以动态在运行环境下打印数据

Go可以通过Println之类的打印数据来调试,但是每次都需要重新编译,这是一件十分麻烦的事情,因此在这里使用GDB来调试程序

GDB是FSF(自由软件基金会)发布的一个强大的类Unix系统下的调试工具,可以做如下事情:

  • 启动程序,可以按照开发者自定义的自定义要求运行程序
  • 可让被调试的程序在开发者设定的调置的断点处停住(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时程序中所发生的事
  • 动态的改变当前程序的执行环境

编译Go程序时需要注意如下几点:

  • 传递参数-ldflags "-s",忽略debug的打印信息
  • 传递-gcflags "-N -l"参数,这样可以忽略Go内部做的一些优化,聚合变量和函数等优化,这样对GDB调试来说非常困难,所以在编译的时候加入这两个参数避免这些优化

下面的内容就省略了,官方文档可见https://golang.org/doc/gdb

现在更好的调试工具是Delve,详情可见:Go调试工具—— Delve

 

3》Go怎么写测试用例

Go自带一个轻量级的测试框架testing和自带的go test 命令来实现单元测试和性能测试

新建一个文件夹gotest,然后创建两个文件gotest.go和gotest_test.go:

1.gotest.go

package gotest

import (
    "errors"
)

func Division(a, b float64) (float64, error) {
    if b == 0{
        return 0, errors.New("divisor can not be 0")
    }
    return a / b, nil
}

2.gotest_test.go

单元测试文件的要求有:

  • 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码
  • 你必须 import `testing`这个包
  • 所有的测试用例函数必须是`Test`开头
  • 测试用例会按照源代码中写的顺序依次执行
  • 测试函数`TestXxx()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态
  • 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。
  • 函数中通过调用`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。详情可见
package gotest

import (
    "testing"
)

func Test_Division_1(t *testing.T) {
    if i, e := Division(6, 2); i != 3 || e != nil {
        t.Error("除法函数测试没通过")
    } else {
        t.Log("第一个测试通过")
    }
}

func Test_Division_2(t *testing.T) {
    t.Error("就是不通过")
}

然后进入gotest文件夹,运行go test :

userdeMBP:gotest user$ go test
--- FAIL: Test_Division_2 (0.00s)
    gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL    _/Users/user/go-learning/gotest    0.006s

显示测试没有通过,这是因为在第二个测试函数中我们写死了测试不通过的代码t.Error

如果要看见详细的测试结果,使用标签-v:

userdeMBP:gotest user$ go test -v
=== RUN   Test_Division_1
--- PASS: Test_Division_1 (0.00s)
    gotest_test.go:11: 第一个测试通过
=== RUN   Test_Division_2
--- FAIL: Test_Division_2 (0.00s)
    gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL    _/Users/user/go-learning/gotest    0.005s

 

3.压力测试

文件原则:

  • 压力测试用例必须遵循如下格式,其中 XXX 可以是任意字母数字的组合,但是首字母不能是小写字母,func BenchmarkXXX(b *testing.B) { ... }
  • go test 不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench="test_name_regex",例如 go test -test.bench=".*"表示测试全部的压力测试函数
  • 在压力测试用例中,请记得在循环体内使用 testing.B.N,以使测试可以正常的运行
  • 文件名也必须以_test.go 结尾
package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { //为了循环使用b.N
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() //调用该函数停止压力测试的时间计数
    //做一些初始化的工作,例如读取文件数据,数据库连接之类的
    //这样这些时间不影响我们测试函数本身的性能

    b.StartTimer() //重新开始时间
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

出错:

userdeMBP:gotest user$ go test -v -bench=. -benchtime=5s webbench_test.go
can't load package: package main: read /Users/user/go-learning/gotest/webbench_test.go: unexpected NUL in inpu

是编码格式问题,使用sublime将其改成UTF-8编码即可,在工具栏file——save with Encoding——UTF-8即可

出错:

userdeMBP:gotest user$ go test -bench="." webbench_test.go 
# command-line-arguments [command-line-arguments.test]
./webbench_test.go:9:9: undefined: Division
./webbench_test.go:20:9: undefined: Division
FAIL    command-line-arguments [build failed]

这是因为webbench_test.go 调用了gotest.go中的Division函数,所以你要把有依赖的函数文件都放一起编译执行才行:

userdeMBP:gotest user$ go test -v ./webbench_test.go ./gotest.go -bench=".*"
goos: darwin
goarch: amd64
Benchmark_Division-8                    2000000000             0.31 ns/op
Benchmark_TimeConsumingFunction-8       2000000000             0.61 ns/op
PASS
ok      command-line-arguments    1.939s

上面的结果可以显示只执行了压力测试函数,第一条显示Benchmark_Division执行了2000000000次,每次执行的平均时间是0.31纳秒;第二条显示了Benchmark_TimeConsumingFunction执行了2000000000次每次执行的平均时间是0.61纳秒;最后一条显示总共的执行时间

 

 

9)部署和维护

 1》应用日志

Go中提供了一个简易的log包,使用它可以方便地实现日志记录的功能。这些日志都是基于fmt包的打印再结合Panic之类的函数来进行一般的打印、抛出错误处理

Go目前的标准包只实现了简单的功能,如果想要把应用日志保存到文件,然后又能够结合日志实现很多复杂的功能,可以使用第三方开发的一个日志系统:http://github.com/cihub/seelog,其实现了很强大的日志功能。

 

seelog是用Go语言实现的一个日志系统,它提供了一些简单的函数来实现复杂的日志分配、过滤和格式化。主要有如下特性:

  • XML的动态配置,可以不用重新编译程序而动态的加载配置信息
  • 支持热更新,能够动态改变配置而不需要重启应用
  • 支持多输出流,能够同时把日志输出到多种流中,如文件流、网络流等
  • 支持不同的日志输出
  1. 命令行输出
  2. 文件输出
  3. 缓存输出
  4. 支持log rotate
  5. SMTP邮件

更多详情可见:

go第三方日志系统-seelog-Basic sections

 go第三方日志系统-seelog-使用文档

当日志写完后,我们想要查找日志中某个特定的日志信息,我们可以使用Linux中的grep等语句,如:

cat /data/logs/roll.log | grep "failed login"

这样就能够查看到日志中与"failed login"内容相关的日志信息了

通过这种方式我们能够很方便地查找相应的信息,这样有利于我们针对应用日志做一些统计和分析。

 

另外还需要考虑的就是日志的大小,对于一个高流量的Web应用来说,日志的增长是十分可怕的,所以在seelog的配置文件中设置了logrorate,即<rollingfile>,这样就能够保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题

 

2》网站错误处理

 web应用中可能出现的错误you:

  • 数据库错误:指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。

    • 连接错误:这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。
    • 查询错误:使用的SQL非法导致错误,这样子SQL错误如果程序经过严格的测试应该可以避免。
    • 数据错误:数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果你的应用程序在上线之前经过了严格的测试也是可以避免这类问题。
  • 应用运行时错误:这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下:

    • 文件系统和权限:应用读取不存在的文件,或者读取没有权限的文件、或者写入一个不允许写入的文件,这些都会导致一个错误。应用读取的文件如果格式不正确也会报错,例如配置文件应该是ini的配置格式,而设置成了json格式就会报错。
    • 第三方应用:如果我们的应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成我们发表一篇文章的功能。
  • HTTP错误:这些错误是根据用户的请求出现的错误,最常见的就是404错误。虽然可能会出现很多不同的错误,但其中比较常见的错误还有401未授权错误(需要认证才能访问的资源)、403禁止错误(不允许用户访问的资源)和503错误(程序内部出错)。

  • 操作系统出错:这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。

  • 网络出错:指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。

错误处理的目标

在实现错误处理之前,我们必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作:

  • 通知访问用户出现错误了:不论出现的是一个系统错误还是用户错误,用户都应当知道Web应用出了问题,用户的这次请求无法正确的完成了。例如,对于用户的错误请求,我们显示一个统一的错误页面(404.html)。出现系统错误时,我们通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。
  • 记录错误:系统出现错误,一般就是我们调用函数的时候返回err不为nil的情况,可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误,则通过邮件通知系统管理员。一般404之类的错误不需要发送邮件,只需要记录到日志系统。
  • 回滚当前的请求操作:如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。
  • 保证现有程序可运行可服务:我们知道没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么我们就需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。

如何处理异常

我们知道在很多其他语言中有try...catch关键词,用来捕获异常情况,但是其实很多错误都是可以预期发生的,而不需要异常处理,应该当做错误来处理,这也是为什么Go语言采用了函数返回错误的设计,这些函数不会panic,例如如果一个文件找不到,os.Open返回一个错误,它不会panic;如果你向一个中断的网络连接写数据,net.Conn系列类型的Write函数返回一个错误,它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败,因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误

但是还有一种情况,有一些操作几乎不可能失败,而且在一些特定的情况下也没有办法返回错误,也无法继续执行,这样情况就应该panic。举个例子:如果一个程序计算x[j],但是j越界了,这部分代码就会导致panic,像这样的一个不可预期严重错误就会引起panic,在默认情况下它会杀掉进程,它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行,发生panic之后,这部分代码后面的函数和代码都不会继续执行,这是Go特意这样设计的,因为要区别于错误和异常,panic其实就是异常处理。如下代码,我们期望通过uid来获取User中的username信息,但是如果uid越界了就会抛出异常,这个时候如果我们没有recover机制,进程就会被杀死,从而导致程序不可服务。因此为了程序的健壮性,在一些地方需要建立recover机制。

func GetUser(uid int) (username string) {
    defer func() {
        if x := recover(); x != nil {
            username = ""
        }
    }()

    username = User[uid]
    return
}

上面介绍了错误和异常的区别,那么我们在开发程序的时候如何来设计呢?规则很简单:如果你定义的函数有可能失败,它就应该返回一个错误。当我调用其他package的函数时,如果这个函数实现的很好,我不需要担心它会panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它。而panic和recover是针对自己开发package里面实现的逻辑,针对一些特殊情况来设计。

 

3》应用部署——使用daemon守护进程实现程序后台持续运行

建议使用Supervisord

Supervisord是用Python实现的一款非常实用的进程管理工具。supervisord会帮你把管理的应用程序转成daemon程序,而且可以方便的通过命令开启、关闭、重启等操作,而且它管理的进程一旦崩溃会自动重启,这样就可以保证程序执行中断后的情况下有自我修复的功能。

⚠️我前面在应用中踩过一个坑,就是因为所有的应用程序都是由Supervisord父进程生出来的,那么当你修改了操作系统的文件描述符之后,别忘记重启Supervisord,光重启下面的应用程序没用。当初我就是系统安装好之后就先装了Supervisord,然后开始部署程序,修改文件描述符,重启程序,以为文件描述符已经是100000了,其实Supervisord这个时候还是默认的1024个,导致他管理的进程所有的描述符也是1024.开放之后压力一上来系统就开始报文件描述符用光了,查了很久才找到这个坑。

学习Supervisord的使用——没弄好

 

4》备份和恢复——文件同步工具rsync 没弄

rsync能够实现网站的备份,不同系统的文件的同步,如果是windows的话,需要windows版本cwrsync。

MySQL备份

应用数据库目前还是MySQL为主流,目前MySQL的备份有两种方式:热备份和冷备份,热备份目前主要是采用master/slave方式(master/slave方式的同步目前主要用于数据库读写分离,也可以用于热备份数据),关于如何配置这方面的资料,大家可以找到很多。冷备份的话就是数据有一定的延迟,但是可以保证该时间段之前的数据完整,例如有些时候可能我们的误操作引起了数据的丢失,那么master/slave模式是无法找回丢失数据的,但是通过冷备份可以部分恢复数据。

冷备份一般使用shell脚本来实现定时备份数据库,然后通过上面介绍rsync同步非本地机房的一台服务器。

MySQL恢复

前面介绍MySQL备份分为热备份和冷备份,热备份主要的目的是为了能够实时的恢复,例如应用服务器出现了硬盘故障,那么我们可以通过修改配置文件把数据库的读取和写入改成slave,这样就可以尽量少时间的中断服务。

但是有时候我们需要通过冷备份的SQL来进行数据恢复,既然有了数据库的备份,就可以通过命令导入:

mysql -u username -p databse < backup.sql

可以看到,导出和导入数据库数据都是相当简单,不过如果还需要管理权限,或者其他的一些字符集的设置的话,可能会稍微复杂一些,但是这些都是可以通过一些命令来完成的。

redis备份

redis是目前我们使用最多的NoSQL,它的备份也分为两种:热备份和冷备份,redis也支持master/slave模式,所以我们的热备份可以通过这种方式实现,相应的配置大家可以参考官方的文档配置,相当的简单。我们这里介绍冷备份的方式:redis其实会定时的把内存里面的缓存数据保存到数据库文件里面,我们备份只要备份相应的文件就可以,就是利用前面介绍的rsync备份到非本地机房就可以实现。

redis恢复

redis的恢复分为热备份恢复和冷备份恢复,热备份恢复的目的和方法同MySQL的恢复一样,只要修改应用的相应的数据库连接即可。

但是有时候我们需要根据冷备份来恢复数据,redis的冷备份恢复其实就是只要把保存的数据库文件copy到redis的工作目录,然后启动redis就可以了,redis在启动的时候会自动加载数据库文件到内存中,启动的速度根据数据库的文件大小来决定。

 

10)web框架——beego

beego的学习——没弄

没学习的部分之后再继续学习,未完待续

posted @ 2019-03-05 19:32  慢行厚积  阅读(1916)  评论(1编辑  收藏  举报