本文介绍golang中常见陷阱和易犯错误。
主要内容是翻译Traps, Gotchas, and Common Mistakes for New Golang Devs博文。

初级

  • 左花括号不能单独放一行
  • 存在未使用变量
    有未使用变量导致编译不通过,注释、删除掉或者赋值给 _ 即可。
  • 未使用import
    IDE配置好即可解决,或者用goimports工具。
  • 变量简短声明(:=)只能在函数内使用
  • 变量简短声明(:=)同一block不能单独重复声明已经存在的变量
    1
    2
    3
    
    one := 0
    one := 1        // error
    one, two := 1,2 // ok
    
  • 变量简短声明(:=)不能作用于结构体字段
    1
    2
    3
    4
    5
    
    type info struct {
        result int
    }
      
    data.result, err := work() //error
    
  • 变量Shadowing
    使用:=可能不小心屏蔽了外层同名变量,导致意外的结果。
    1
    2
    3
    4
    5
    6
    
    x := 1
    {
        x := 2
        fmt.Println(x) //prints 2
    }
    fmt.Println(x)     //prints 1
    
  • 不能使用nil初始化未指定类型的变量
    1
    
    var x = nil //error
    
  • 直接使用nil的slice和map
    一般make之后再使用。
    1
    2
    3
    4
    5
    
    var s []int
    s = append(s,1) // ok
      
    var m map[string]int
    m["one"] = 1    // error
    
  • map容量
    make map时可以指定容量,不过不能对其调用cap。
  • 字符串不能赋值为nil
    1
    2
    
    var s1 string = nil // error
    var s2 string       // ok, defaults to "" 
    
  • 数组类型函数参数
    (静态)数组作为函数参数是值拷贝,函数内的修改不作用于原来的数组。要修改可以用slice或指针。
  • range遍历slice或map
    range遍历slice或map时遍历变量第一个是索引,第二个才是数据(值拷贝)。
  • slice和数组是一维的
    动态多维数组可以模拟,但是使用上不是很方便。
    1
    2
    3
    4
    5
    6
    7
    
    x := 2
    y := 4
    
    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
    
  • 读取map中不存在的键值
    不返回nil而是默认零值。判断键是否存在需要检查第二个返回值。
  • 字符串是只读的
    不能更新字符串中的字符,需要转换成字节slice才能操作。
  • 字符串和字节slice间的转换
    转换是完整的拷贝,而不是其它语言中的强制类型转换。
  • 字符串索引访问
    返回的是字节而不是字符(可能是多个字节)。
    需要访问字符的话可以用for range语句或者utf8包。
  • 字符串并不总是UTF-8编码
    字符串字面量是UTF-8编码,字符串变量可以是任意编码(如UTF-16)。
  • 字符串长度
    len函数返回的是字节数而不是字符数。字符数可以通过对应编码包的接口获取。
  • 多行slice、数组、map字面量遗漏逗号
    1
    2
    3
    4
    
    x := []int{
        1,
        2 //error
    }
    
  • log.Fatal和log.Panic不仅输出日志,还会终止程序
  • 大部分内置数据类型操作并不是并发安全的
    需要channel或者sync包确保并发安全。
  • range遍历字符串
    索引是每个字符第一个字节的索引。默认按照UTF-8编码解析。
  • range遍历map
    遍历顺序并不固定。
  • switch语句fall through
    case分支默认break而不是fall through。
    需要fall through可以使用fallthrough或写在同一case。
  • 自增、自减
    是语句而不是表达式,没有值,不能用于表达式中。
  • 取反、异或运算符都是^
  • 运算符优先级差异
    不确定就加括号。
  • 未导出结构体字段不会编码
    小写字段不会编码。
  • 程序退出时仍有活跃协程
    可以通过channel、WaitGroup(不能复制!)等手段等待其它协程都结束才退出。
  • 发送数据到已关闭channel会panic
  • 使用nil的channel
    对nil的channel进行收、发操作会导致阻塞、死锁。
    配合select使用可以实现开启、关闭特定case分支的效果。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    inch := make(chan int)
    outch := make(chan int)
    
    go func() {
        var in <- chan int = inch
        var out chan <- int
        var val int
          
        for {
            select {
            case out <- val:
                out = nil
                in = inch
            case val = <- in:
                out = outch
                in = nil
            }
        }
    }()
    
  • 传值接收者方法不会修改接收者的值
    需要传指针才可以修改。引用类型的数据结构(如slice, map)本质上就是传指针。

中级

  • 关闭HTTP Response Body
    1
    2
    3
    4
    5
    6
    7
    8
    
    resp, err := http.Get("https://golang.org")
    if resp != nil {
        defer resp.Body.Close()
    }
    if err != nil {
        // error handling
        return
    }
    
  • 关闭HTTP连接
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    req, err := http.NewRequest("GET", "https://golang.org", nil)
    if err != nil {
        // 处理出错情况
        return
    }
      
    req.Close = true
    // 或者这样处理:
    //req.Header.Add("Connection", "close")
    
  • 官方JSON编码器会自动添加\n
  • 官方JSON包默认会转义特殊HTML字符
  • 官方JSON反序列化(unmarshal)数字默认类型为float64,包括整数
  • 官方JSON字符串值不能有十六进制或者非UTF-8编码转义序列
  • 比较结构体、数组、slice和map
    相同类型且元素或所有字段本身可比较的数组或结构体才能用==比较,slice、map不能比较。
    不能==比较则使用helper函数(如reflect.DeepEqual,bytes.Equals,strings.EqualFold)比较。
  • 从panic恢复
    在defer的函数里直接调用recover才能从panic恢复。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    func doRecover() {  
        fmt.Println("recovered =>", recover()) //prints: recovered => <nil>
    }
      
    func submit() {
        defer func() {
            doRecover() //panic is not recovered
        }()
          
        // ...
        panic(err)
    }
    
  • range语句中更新、引用slice,数组,map的元素值
    循环变量只是元素的值拷贝,对其取地址、更新都不会符合编码预期。
    通过索引运算符才能正确取地址、更新元素值。元素直接存指针也可以(增加GC负担)。
    1
    2
    3
    4
    5
    6
    7
    
    data := []int{1,2,3}
    for _, v := range data {
        v *= 10 // original item is not changed
        //data[i] *= 10
    }
      
    fmt.Println("data:", data) //prints data: [1 2 3]
    
  • slice中隐藏的数据
    对slice切片返回的新slice会共享其底层数组,可能导致错误的内存使用(读取原slice其它内容)。
    1
    2
    3
    4
    5
    6
    
    raw := make([]byte, 64)
    copy(raw, "public secret")
    field := raw[:6]
    
    fmt.Println("field:", string(field))
    fmt.Println("all:", string(field[:cap(field)]))
    

    通过copy需要的数据来避免上述情况。

    1
    2
    3
    4
    5
    6
    7
    8
    
    func getField() []byte {
        raw := make([]byte, 10000)
        res := make([]byte, 6)
          
        copy(res, raw[:6])
          
        return res
    }
    
  • slice数据污染
    对slice切片返回的新slice会共享其底层数组,可能导致错误的内存使用(修改原slice内容)。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/')
    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    	
    fmt.Println("dir1 =>", string(dir1)) // prints: dir1 => AAAA
    fmt.Println("dir2 =>", string(dir2)) // prints: dir2 => BBBBBBBBB
    
    dir1 = append(dir1, "suffix"...)
    fmt.Println("dir2 =>", string(dir2)) // prints: dir2 => uffixBBBB
    

    可以采取拷贝副本的方法解决,也可以通过切片操作的第3个参数控制容量。
    dir1 := path[:sepIndex:sepIndex]写超过容量会触发分配新的缓冲区而不是部分覆盖dir2

  • 过期的slice
    多个slice共享同一底层数组,发生扩容时会导致其它slice指向旧的数据。
  • 类型声明和方法
    基于已有类型(非接口类型)声明新类型,新类型不能调用原有类型的方法。
    1
    2
    3
    4
    
    type myMutex sync.Mutex
      
    var mtx myMutex
    mtx.Lock() // error
    

    可以通过匿名字段嵌入的方式得到(不是继承,是组合!)原类型的方法:

    1
    2
    3
    4
    5
    6
    
    type myLocker struct {  
        sync.Mutex
    }
      
    var lock myLocker
    lock.Lock() //ok
    
  • 跳出for switchfor select代码块
    通过label跳出外部循环。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    loop:
        for {
            switch {
            case true:
                fmt.Println("breaking out...")
                break loop
            }
        }
      
        fmt.Println("out!")
    
  • for循环中的迭代变量和闭包
    每轮循环都是同一迭代变量,赋予新的值。
    闭包捕获同一变量,很可能不符合编程预期。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    data := []string{"one", "two", "three"}
    
    for _, v := range data {
        go func() {
            fmt.Println(v)
        }()
    }
      
    time.Sleep(3 * time.Second)
    // goroutines print: three, three, three
    

    解决方法一:赋值给循环体内新变量:

    1
    2
    3
    4
    5
    6
    7
    
    for _, v := range data {
        vcopy := v
          
        go func() {
          fmt.Println(vcopy)
        }()
    }
    

    解决方法二:迭代变量作为参数传递给闭包:

    1
    2
    3
    4
    5
    
    for _, v := range data {
        go func(in string) {
          fmt.Println(in)
        }(v)
    }
    

    比较隐蔽的情况(接收者本质就是参数):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    type field struct {
        name string
    }
      
    func (p *field) print() {
        fmt.Println(p.name)
    }
      
    data1 := []field{ {"one"}, {"two"}, {"three"} }  // values
    data2 := []*field{ {"one"}, {"two"}, {"three"} } // pointers
      
    for _, v := range data1 {
        go v.print()
    }
    time.Sleep(1 * time.Second)
      
    for _, v := range data2 {
        go v.print()
    }
    time.Sleep(1 * time.Second)
    
  • defer函数参数求值时机
    defer语句求值时,defer函数的参数就会求值(而不是函数运行时)。
  • defer函数执行时机和顺序
    defer函数在所在函数即将结束返回时执行,有多个defer函数则以后进先出顺序执行。
    在for循环体中defer释放资源操作不是合适的做法,应该包装到函数内或者用完直接释放资源。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    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...
          }()
    }
    
  • 类型断言失败
    类型断言失败返回对应断言类型的零值。再加上变量shadowing就会产生更迷惑的效果。
    1
    2
    3
    4
    5
    6
    7
    8
    
    var data interface{} = "confusing"
      
    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 "confusing")
    }
    
  • 阻塞的协程与资源泄露
    从数据库集群拉取最先返回的结果,可行但是有bug的实现如下:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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
    }
    

    由于channel无缓冲,所以最快的协程正常退出,其他都永久阻塞在返回结果那一步,造成资源泄露。
    解决方法一:创建有足够缓冲的channel:c := make(chan Result, len(replicas))
    解决方法二:selece+default+1格缓冲的channel:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    func First(query string, replicas ...Search) Result {
        c := make(chan Result, 1)
        searchReplica := func(i int) {
            select {
            case c <- replicas[i](query):
            default:
            }
        }
          
        for i := range replicas {
            go searchReplica(i)
        }
          
        return <-c
    }
    

    解决方法三:关闭channel通知协程:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    func First(query string, replicas ...Search) Result {  
        c := make(chan Result)
        done := make(chan struct{})
        defer close(done)
        searchReplica := func(i int) { 
            select {
            case c <- replicas[i](query):
            case <- done:
            }
        }
          
        for i := range replicas {
            go searchReplica(i)
        }
          
        return <-c
    }
    
  • 不同的0字节变量分配相同的地址
  • 首次使用iota并不总是从0开始
    iota是const定义块内当前行的索引,不是第一行就不是0。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    const (
        azero = iota
        aone  = iota
    )
    const (
        info  = "processing"
        bzero = iota
        bone  = iota
    )
      
    fmt.Println(azero, aone) //prints: 0 1
    fmt.Println(bzero, bone) //prints: 1 2
    

高级

  • 值实例调用指针接收者方法
    只要值本身是可取地址,就可以调用指针接收者方法。
    map的元素不可取地址,接口引用的变量也是不可取地址的。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    type data struct {
        name string
    }
    func (p *data) print() {
        fmt.Println("name:", p.name)
    }
    type printer interface {
        print()
    }
      
    d1 := data{"one"}
    d1.print() // 合法
      
    var in printer = data{"two"} // 错误。 *data实现了printer接口,但是data没有
    in.print()
      
    m := map[string]data { "x": data{"three"} }
    m["x"].print() // 错误,map的元素不可取地址
    
  • 更新map的值类型元素
    如果map的元素是结构体(非指针类型),那么不能单独更新结构体的字段值。
    这是因为map的元素不可取址。
    不过令人迷惑的是slice支持这样的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    type data struct {  
        name string
    }
    m := map[string]data{ "x":{"one"} }
    m["x"].name = "two" // error
      
    s := []data{ {"one"} }
    s[0].name = "two" // ok
    fmt.Println(s)    // prints: [{two}]
    
  • nil接口和nil接口值
    接口变量为nil当且仅当其type和value字段都为nil。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    var data *byte
    var in interface{}
      
    fmt.Println(data, data == nil) // prints: <nil> true
    fmt.Println(in, in == nil)     // prints: <nil> true
      
    in = data
    fmt.Println(in, in == nil)     // prints: <nil> false
    // 'data' is 'nil', but 'in' is not 'nil'
    

    返回接口类型的函数尤其需要注意这种情况,明确地return nil

  • 栈和堆变量
    跟C++不同,变量分配在堆上和栈上不由关键字决定。
    Go会根据变量内存占用大小以及逃逸分析(可以参考这里)决定将变量分配到哪。

  • GOMAXPROCS, Concurrency和Parallelism
    具体参考runtime的说明以及Go的调度模型。

    The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

  • 读写操作(指令)重排
    Go编译器会在保证协程内(不包括协程间)最终结果的前提下重排读写指令(现代CPU也会指令重排)。
    比如说u1内b并不依赖a,所以可能先执行b = 2然后再执行a = 1

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    var a, b int
      
    func u1() {
        a = 1
        b = 2
    }
    func u2() {
        a = 3
        b = 4
    }
    func p() {
        println(a)
        println(b)
    }
      
    func main() {
        go u1()
        go u2()
        go p()
          
        time.Sleep(1 * time.Second)
    }
    
  • 抢占式调度
    在老版本Go(单线程)中如果有协程死循环而且没有触发(显式或隐式)调度的代码,那么程序就会死锁。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    var _ = runtime.GOMAXPROCS(1) // 设置成1新版本Go下面的代码才会死锁
      
    func main() {
        done := false
          
        go func(){
          done = true
        }()
          
        for !done {
            // 空或者没有触发调度的代码
            
            //time.Sleep(time.Second)
            //runtime.Gosched()
        }
          
        fmt.Println("done!")
    }
    

    解决方法是for循环里面显式调用runtime.Gosched()或者其他阻塞操作(如time.Sleep)。
    当然新版本的Go和现代CPU默认情况下并不需要特别处理也不会死锁。

  • import “C"和import块
    需要imort C包才能使用cgo,不能和其他包混在一起写。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    /*
    #include <stdlib.h>
    */
    import (
        "C"
    )
      
    import (
        "unsafe"
    )
    
  • import “C"和cgo注释之间不能有空行

  • 不能调用C语言变参函数
    需要改为定参函数。