使用 go 语言可能常忽略的几十种情况 - 中级进阶

本文的整理于 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs, 下面是自己阅读完源文章后翻译了一些以及加上自己的一点见解。
这些情况使用于 Go 1.5 以及更低版本

目录 #


关闭 HTTP 返回的主体 (Response Body) 问题 #

对于 http 返回的 body 即使你不读取也要调用 Close 关闭,这样可以保证在开启了长连接时 http 连接可以被重用,
推荐关闭是是使用defer, 而且要在对返回的 error 进行判断是否为 nil 后才调用 defer(即 defer response.Close() 应该置于 if err!=nil{}后面), 否则在函数返回时调用 resonse.Close 会产生 panic(对一个 nil 执行 Close 操作是不允许的), 参考 stackOverFlow 问答
如果连接重用对于程序来说很重要,你可能需要在函数的后面加上

_, err = io.Copy(ioutil.Discard, resp.Body)

这个很有必要,因为如果代码中没有对整个 body 进行读取数据,比如使用 json 对数据进行转换json.NewDecoder(resp.Body).Decode(&data)时就有可能造成连接的不可重用


关闭 HTTP 连接问题 #

有时候不想重用 HTTP 连接,比如隔一段时间向不同服务器发送少量的 http 请求,可以把 request 的 Close 设置为 true
或者在 HTTP 头部加上 req.Header.Add("Connection", "close")
亦或者把长连接进行一个全局的关闭,需要创建一个http.Transport,如下

tr := &http.Transport{DisableKeepAlives: true}
    client := &http.Client{Transport: tr}

    resp, err := client.Get("http://golang.org")
    if resp != nil {
        defer resp.Body.Close()
    }

但如果与相同的服务器进行频繁的 HTTP 请求交互,还是把 KeepAlive 启用,且不要关闭 Connection 为好


把数字类型的 Json 存放到 Interface 问题 #

如果不指定 struct 变量对应的 json 类型,对于 json 的数字数据在 decode 时会转换成 float64 的类型,比如下面

  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  //var status = result["status"].(int) //error
  var status = result["status"].(float64) //ok
  fmt.Println("status value:",status)

另一种来获取想要的数据类型的方法是 使用 Decoder, 把 json 数字转换成 json.Number 再直接转换成想要的类型,如

var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  var decoder = json.NewDecoder(bytes.NewReader(data))
  decoder.UseNumber()

  if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status,_ = result["status"].(json.Number).Int64() //json.Numer->Int64
  fmt.Println("status value:",status)

比较 Struts,Arrays,Slices 和 Maps #

一般来说只有基础的类型变量才可以直接使用 [==] 来比较的,而数组只有在元素个数相同时可以直接比较,Go 语言提供一系列的帮助函数来比较那些不能直接使用 [==] 来比较的变量,最常用的的reflect包中的DeepEqual()

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

DeepEqual() 示例
DeepEqual()并不会把 nil Slice 和 empty Slice 视作一样,而 bytes.Equal() 却把 “nil”和 “empty” Slice 同等看待

    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true

bytes.Equal() 示例
DeepEqual()在比较 Slice 或者 map 时并不总是靠谱的

    v1 := []string{"one","two"}
    v2 := []interface{}{"one","two"}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
    //prints: v1 == v2: false (not ok)

    data := map[string]interface{}{
        "code": 200,
        "value": []string{"one","two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
    //prints: data == decoded: false (not ok)

DeepEqual() 比较 slice,map 示例
在(使用 ==,bytes.Equal(), bytes.Compare()) 比较时防止字母大小问题,记得使用bytes或者strins包中的ToUpper()或者ToLower()来转换,而对于包含有其它语言的字符测应该使用string.EqualFold()bytes.EqualFold()
如果 Slice 里面包含了一些密文如以 cryptographic hashes,tokens, 这些往往需要大量计算,造成 timing attacks, 可以使用crypto/subtle包中的函数如subtle.ConstantTimeCompare()来避免这类问题


从 panic 中恢复问题 #

recover() 要配合 defer 来使用,且调用的 defer() 时必须与 panic() 处于同一级函数
或者在上级函数中,因为 panic 在没有被捕获时是一层一层往上级传递的
注意 recover 在 defer 调用的函数的位置不可嵌于其它函数中,否则不会生效
还有 recover 的 defer 代码必须置于 panic() 之前

func pp() {
	recover() //work

	//not work
	// func() {
	// 	defer recover()
	// }()
}

main{
	defer pp()
	panic("ddd")

	//not execute
	//defer pp()
}

使用 range 对 Slice,Array 和 Map 的引用值进行更新问题 #

对于值类型的 slice 在 range 进所得到 value 是源值一个 copy, 比如想要更新一个 int slice 这样是不行的

    data := []int{1,2,3}
    for _,v := range data {
        v *= 10 //original item is not changed
    }

    fmt.Println("data:",data) //prints data: [1 2 3]

当然可以用下标指定更新的元素

data := []int{1,2,3}
    for i,_ := range data {
        data[i] *= 10
    }

如果是一组指针 slice, 指针的值是地址,地址也是值,但更新数据时是源地址的数据故在 range 的更新会有效,

    data := []*struct{num int} {{1},{2},{3}}

    for _,v := range data {
        v.num *= 10
    }

    fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}

Slice 中数据隐藏的问题 #

要知道 slice 变量的底层 (underlying array) 也是一个数组
像这种情况,在任何一个 slice 中更改数据都将会影响到底层的数组,

	raw := make([]int, 10)
	fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
	a := raw[:3]
	a[1] = 988
	fmt.Printf("a: %+v\n", a)
	fmt.Printf("raw: %+v\n", raw)

若想要更新时不影响原来的 slice, 应该新建一个 slice, 然后从源 slice 中 copy 想要的数据

	raw := make([]int, 10)
	fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
	a := make([]int,3)
	copy(a,raw[:3]
	fmt.Printf("a: %+v\n", a)
	fmt.Printf("raw: %+v\n", raw)

或者从源 slice 切分时加上最后一个容量限制大小 (full slice expression), 如

	raw := make([]int, 10)
	fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
	// a := raw[:3]
	a := raw[:3:3]
	a[1] = 988

	a = append(a, 999)
	//vs
	//a[2] = 988
	//a = a[:2:2]
	//a[1] = 1111

	fmt.Printf("a: %+v,cap(a):%d,len(a):%d arrd:%v \n", a, cap(a), len(a), &a[0])
	fmt.Printf("raw: %+v,cap(raw):%d,len(raw):%d arrd:%v \n", raw, cap(raw), len(raw), &raw[0])

注意使用 a:=raw[:len:cap]\(这里的 cap 是原 slice 的长度,len 自取)这种全切分方式获取到的 slice 可能指向源 slice 的底层数组,若对 a 进行append 新的数据即增加数据时,a 会指向新的并且是自动扩展后复制了原来数据的底层数组, 否则 a 还是和源 slice 一样, 双方的操作都会互相影响
这文章 很值得阅读和理解。


Slice 的伸缩问题 #

这个问题要掌握的知识和 Slice 中数据隐藏的问题一样,都是 slice 中对底层数组的理解


注意类型声明和方法 #

如果你从一种结构类型中定义新的类型,新的类型并不会继承源类型的方法,如

type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock() //error
    mtx.Unlock() //error
}

可以使用匿名成员方式把这个源结构包含在结构中来解决

type myLocker struct {
    sync.Mutex
}

func main() {
    var lock myLocker
    lock.Lock() //ok
    lock.Unlock() //ok
}

此时 myLocker 已经和 sync.Mutex 一样可以使用相同的方法,它们都实现同样的接口(方法), 所以在使用时也可以这样初始化

type myLocker sync.Locker

func main() {
    var lock myLocker = new(sync.Mutex)
    lock.Lock() //ok
    lock.Unlock() //ok
}

理解’for switch’和’for select’ #

在 switch 或者 select 中使用不带后缀 label 的 break 会跳出整个 loop, 如果不想使用 return,break label的方式是不错的选择,当然goto也可以在后面加 label
break label 示例


变量的遍历和闭包在 for 中使用要注意的问题 #

    data := []string{"one","two","three"}

    for _,v := range data {
        vcopy := v //
        go func() {
            fmt.Println(vcopy)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three

上面中的 v 在整个 for range loop 中是共用的(不太好解释), 故最后显示的是最后一个 three,
若要显示 slice 中的每个元素,在 loop 应该使用新的变量,这个每一个 goroutine 中的函数(或者匿名函数)所使用的变量才不是同一个数据
而像这种情况

    data := []string{"one","two","three"}

    for _,v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three

因为 v 是作为函数的参数传入,是原来值的 copy, 如不管 v 在 loop 过程改变了,函数中的参数值还是最初传进来的那个
不过下面就不太好解释了,有点复杂
for trap 示例
引用类型的实例与非引用的实例在 for range loop 里面调用方法的结果不一样!!! 怎么解释?
最好还是使用 fori loop 安全一点, 突然想起了康力 说过”我一般不用 for range”


函数参数在使用 defer 时要注意的问题 #

    var i int = 1

    defer fmt.Println("result =>",func() int { return i * 2 }())
    i++
    //prints: result => 2 (not ok if you expected 4)

(defer)[https://play.golang.org/p/3RmaKGDHqF]
这里要注意 defer 中使用的 i 是相对的属于全局变量,在 i++ 之前 i 为 1 时,就与保存在 defer 中
再看这个
这与变量的遍历和闭包在 for 中使用要注意的问题 有一定的相同点,但要明白,defer 在是函数退出时才执行, 但其所使用的变量值却不一定就是各个相关变量在函数执行后的值


执行函数在使用 defer 时要注意的问题 #

正如函数参数在使用 defer 时要注意的问题所提到,defer 在函数退出时执行而不是在最后一行代码执行完执行,如里在 for loop 中使用 defer 容易出错, 如

    for _,target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
            break
        }
        defer f.Close() //will not be closed at the end of this code block
        //do something with the file...
    }

可以使用一个函数来包裹 defer:

    for _,target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:",target,"error:",err)
                return
            }
            defer f.Close() //ok
            //do something with the file...
        }()

类型断言失败时数据处理问题 #

在对 interface 进行 type assert 时,可能有人喜欢用旧的变量名来保存要 type assert 的变量返回值,如

    var data interface{} = "great"

    if data, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",data)
    } else {
        fmt.Println("[not an int] value =>",data)
        //prints: [not an int] value => 0 (not "great")
    }

正确的方式应该使用新的变量来保存返回值:

    if res, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",res)
    } else {
        fmt.Println("[not an int] value =>",data)
        //prints: [not an int] value => great (as expected)
    }

Goroutine 堵塞和资源泄漏问题 #

这一部分关系到 goroutine 和 channel 通信处理问题,看下面的示例

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

这是从多个目标中获取第一个结果,存在的问题为,多个 goroutine 往同一个 channel 写数据, 但最后会只有一个数据可以返回, 这样的话其它的 goroutine 会被堵塞 (channel 的 buffer 默认为 1), 最后造成资源的泄漏,
解决的一个方法,生成足够大的 channel buffer:

    c := make(chan Result,len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c

另一个方法是使用select来防止写 channel 堵塞:

        searchReplica := func(i int) {
        select {
        case c <- replicas[i](query):
        default:
        }
    }

还有另外一个方法是 select 一个 channel 来判断是否退出 goroutine:

    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) {
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }

这里使用 struct{}类型的 channel 是因为这种类型不需要占用空间

2016-07-01