使用 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