本文翻译自最近各种 Go 语言社区分享的很多的英文文档 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs,小编第一眼以为 50 Shades of Grey……

Go语言是一个简单却蕴含深意的语言。但是,即便号称是最简单的C语言,都能总结出一本《C陷阱与缺陷》,更何况Go语言呢。Go语言中的许多坑其实并不是因为Go自身的问题。一些错误你再别的语言中也会犯,例如作用域,一些错误就是对因为 Go 语言的特性不了解而导致的,例如 range。

其实如果你在学习Go语言的时候去认真地阅读官方文档,百科,邮件列表或者其他的类似 Rob Pike 的名人博客,报告,那么本文中提到的许多坑都可以避免。但是不是每个人都会从基础学起,例如译者就喜欢简单粗暴地直接用Go语言写程序。如果你也像译者一样,那么你应该读一下这篇文章:这样可以避免在调试程序时浪费过多时间。

本文将50个坑按照使用使用范围和难易程度分为以下三个级别:“新手入门级”,“新手深入级”,“新手进阶级”。

“{”不能单独放在一行
级别:新手入门级

Go语言设计者肯定和C语言设计者(K&R)有种不明不白的关系,因为C语言中的K&R格式在Go语言中得到发扬光大。大多数语言中,大括号中的左括号是可以随便放在哪里的:C语言中必须要按照K&R格式对代码进行格式化之后,左括号才会被放在前一行中的最后。但是Go语言中,左括号必须强制不能单独放在一行。这个规则得益于“自动分号注射”(automatic semicolon injection)。

补充:go提供了专门用于格式化代码的gofmt工具。

出错代码:

package main

import "fmt"

func main()  
{ //error, can't have the opening brace on a separate line
    fmt.Println("hello there!")
}

错误信息:

/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {
修正代码:

package main

import "fmt"

func main() {  
    fmt.Println("works!")
}

未使用已定义的变量
级别:新手入门级

如果代码中有未使用的变量,那个代码编译的时候就会报错。Go要求在代码中所有声明的变量都需要被用到,当然,全局变量除外。 函数的参数也可以只被声明,不被使用。

对于未声明变量的调用同样会导致编译失败。和C语言一样,Go编译器也是个女人,他说什么你都要尽力满足。

出错代码:

package main

var gvar int //not an error

func main() {  
    var one int   //error, unused variable
    two := 2      //error, unused variable
    var three int //error, even though it's assigned 3 on the next line
    three = 3

    func(unused string) {
        fmt.Println("Unused arg. No compile error")
    }("what?")
}

错误信息:

/tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used

修正代码:

package main

import "fmt"

func main() {  
    var one int
    _ = one

    two := 2 
    fmt.Println(two)

    var three int 
    three = 3
    one = three

    var four int
    four = four
}

当然,你也可以考虑删除那些没有使用的变量。

未使用的包
级别:新手入门级

当import一个包之后,如果不使用这个包,或者这个包中的函数/接口/数据结构/变量,那么将会编译失败。

如果真的确认要引入变量但是不使用的话,我们可以用“”标识符坐标记,避免编译失败。“”标识符表示为了得到这些包的副作用而引入这些包。

出错代码:

package main

import (  
    "fmt"
    "log"
    "time"
)

func main() {  
}

错误信息:

/tmp/sandbox627475386/main.go:4: imported and not used: "fmt" 
/tmp/sandbox627475386/main.go:5: imported and not used: "log" 
/tmp/sandbox627475386/main.go:6: imported and not used: "time"

修正代码


package main

import (  
    _ "fmt"
    "log"
    "time"
)

var _ = log.Println

func main() {  
    _ = time.Now
}

只能在函数内部使用简短的变量声明
级别:新手入门级 出错代码:

package main

myvar := 1 //error

func main() {  
}

错误信息:

/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

修正代码:

package main

var myvar = 1

func main() {  
}

无法使用精简的赋值语句对变量重新赋值
级别:新手入门级

不能使用精简的赋值语句重新赋值单个变量,但是可以使用精简的赋值语句同时赋值多个变量。

并且,重定义的变量必须写在同一个代码块。

错误信息:

package main

func main() {  
    one := 0
    one := 1 //error
}

错误信息:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

修正代码:

package main

func main() {  
    one := 0
    one, two := 1,2

    one,two = two,one
}

隐式变量(作用域)
级别:新手入门级

和 C 语言一样,Go 语言也有作用于,一个变量的作用范围仅仅是一个代码块。虽然精简的赋值语句很简单,但是注意作用域。

package main

import "fmt"

func main() {  
    x := 1
    fmt.Println(x)     //打印 1
    {
        fmt.Println(x) //打印 1
        x := 2
        fmt.Println(x) //打印 2
    }
    fmt.Println(x)     //打印 1 ( 不是 2)
}

甚至对于有经验的开发者来说,这也是个不注意就会掉进去的深坑。

除非特别指定,否则无法使用 nil 对变量赋值
级别:新手入门级

nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。

错误信息:

package main

func main() {  
    var x = nil //error

    _ = x
}

错误信息:

/tmp/sandbox188239583/main.go:4: use of untyped nil
修正代码:

package main

func main() {  
    var x interface{} = nil

    _ = x
}

Slice 和 Map 的 nil 值
级别:新手入门级

初始值为 nil 的 Slice 是可以进行“添加”操作的,但是对于 Map 的“添加”操作会导致运行时恐慌。( ﹁ ﹁ ) 恐慌。

修正代码:

package main

func main() {  
    var s []int
    s = append(s,1)
}

错误信息:

package main

func main() {  
    var m map[string]int
    m["one"] = 1 //error

}

Map 定长
级别:新手入门级

创建 Map 的时候可以指定 Map 的长度,但是在运行时是无法使用 cap() 功能重新指定 Map 的大小,Map 是定长的。

错误信息:

package main

func main() {  
    m := make(map[string]int,99)
    cap(m) //error
}

错误信息:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap
字符串无法为 nil
级别:新手入门级

所有的开发者都可能踩的坑,在 C 语言中是可以 char *String=NULL,但是 Go 语言中就无法赋值为 nil。

错误信息:

package main

func main() {  
    var x string = nil //error

    if x == nil { //error
        x = "default"
    }
}

Compile Errors:

/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)
修正代码:

package main

func main() {  
    var x string //defaults to "" (zero value)

    if x == "" {
        x = "default"
    }
}

参数中的数组
Array Function Arguments

级别:新手入门级 对于 C 和 C++ 开发者来说,数组就是指针。给函数传递数组就是传递内存地址,对数组的修改就是对原地址数据的修改。但是 Go 语言中,传递的数组不是内存地址,而是原数组的拷贝,所以是无法通过传递数组的方法去修改原地址的数据的。

package main

import "fmt"

func main() {  
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])
}

如果需要修改原数组的数据,需要使用数组指针(array pointer)。

package main

import "fmt"

func main() {  
    x := [3]int{1,2,3}

    func(arr *[3]int) {
        (*arr)[0] = 7
        fmt.Println(arr) //prints &[7 2 3]
    }(&x)

    fmt.Println(x) //prints [7 2 3]
}

或者可以使用 Slice,

package main

import "fmt"

func main() {  
    x := []int{1,2,3}

    func(arr []int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [7 2 3]
}

使用 Slice 和 Array 的 range 会导致预料外的结果
级别:新手入门级

如果你对别的语言中的 for in 和 foreach 熟悉的话,那么 Go 中的 range 使用方法完全不一样。因为每次的 range 都会返回两个值,第一个值是在 Slice 和 Array 中的编号,第二个是对应的数据。

出错代码:

package main

import "fmt"

func main() {  
    x := []string{"a","b","c"}

    for v := range x {
        fmt.Println(v) //prints 0, 1, 2
    }
}

修正代码:

package main

import "fmt"

func main() {  
    x := []string{"a","b","c"}

    for _, v := range x {
        fmt.Println(v) //prints a, b, c
    }
}

Slice 和 Array 维度是一维
级别:新手入门级

Go 看上去支持多维的 Array 和 Slice,但是其实不然。尽管可以创建 Array 的 Array,也可以创建 Slice 的 Slice。对于依赖多维 Array 的计算密集型的程序,无论是从性能还是复杂程度,Go 都不是最佳选择。

当然,如果你选择创建嵌套的 Array 与嵌套的 Slice,那么你就得自己负责进行索引、进行下表检查、以及 Array 增长时的内存分配。嵌套 Slice 分为两种,Slice 中嵌套独立的 Slice,或者 Slice 中嵌套共享数据的 Slice。

使用嵌套的独立 Slice 创建多维的 Array 需要两步。第一步,创建外围 Slice,然后分配每个内部的 Slice。内部的 Slice 是独立的,可以对每个单独的内部 Slice 进行缩放。

package main

func main() {  
    x := 2
    y := 4

    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
}

使用嵌套、共享数据的 Slice 创建多维 Array 需要三步。第一,创建数据“容器”,第二部,创建外围 Slice,第三部,对内部的 Slice 进行初始化。


package main

import "fmt"

func main() {  
    h, w := 2, 4

    raw := make([]int,h*w)
    for i := range raw {
        raw[i] = i
    }
    fmt.Println(raw,&raw[4])
    //prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

    table := make([][]int,h)
    for i:= range table {
        table[i] = raw[i*w:i*w + w]
    }

    fmt.Println(table,&table[1][0])
    //prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

Go 语言也有对于支持多维 Array 和 Slice 的提案,不过不要期待太多。Go 语言官方将这些需求分在“低优先级”组中。

试图访问不存在的 Map 键值
级别:新手入门级

并不能在所有情况下都能通过判断 map 的记录值是不是 nil 判断记录是否存在。在 Go 语言中,对于“零值”是 nil 的数据类型可以这样判断,但是其他的数据类型不可以。简而言之,这种做法并不可靠(例如布尔变量的“零值”是 false)。最可靠的做法是检查 map 记录的第二返回值。 错误代码:

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if v := x["two"]; v == "" { //incorrect
        fmt.Println("no entry")
    }
}

修正代码:

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

String 不可变
级别:新手入门级

对于 String 中单个字符的操作会导致编译失败。String 是带有一些附加属性的只读的字节片(Byte Slices)。所以如果想要对 String 操作的话,应当使用字节片操作,而不是将它转换为 String 类型。

错误信息:

package main

import "fmt"

func main() {  
    x := "text"
    x[0] = 'T'

    fmt.Println(x)
}

错误信息:

/tmp/sandbox305565531/main.go:7: cannot assign to x[0]
修正代码:

package main

import "fmt"

func main() {  
    x := "text"
    xbytes := []byte(x)
    xbytes[0] = 'T'

    fmt.Println(string(xbytes)) //prints Text
}

注意这里的操作并不就是最正确的操作,因为有些字符可能会存储在多个字节中。如果你的开发情景有这种情况的话,需要先把 String 转换为 rune 格式。即便是 rune,一个字符也可能会保存在多个 rune 中,因此 Go String 也表现为字节序列(String Sequences)。Rune 一般理解为“字符”,“一个字符也可能会保存在多个 rune 中”的情况我们将会在下面提到。

String 与 Byte Slice 的转换
级别:新手入门级

当将 String 类型和 Byte Slice 类型互相转化的时候,得到的新数据都是原数据的拷贝,而不是原数据。类型转化和切片重组(Resliciing)不一样,切片重组后的变量仍然指向原变量,而类型转换后的变量指向原变量的拷贝。

Go 语言已经对 []byte 和 String 类型的互相转化做了优化,并且还会继续优化。

The first optimization avoids extra allocations when []byte keys are used to lookup entries in map[string] collections: m[string(key)]. 一个优化是

The second optimization avoids extra allocations in for range clauses where strings are converted to []byte: for i,v := range []byte(str) {…}.

String 与下标
级别:新手入门级

和其他语言不同,String 的下表返回值是 Byte 类型的值,而不是字符类型。

package main

import "fmt"

func main() {  
    x := "text"
    fmt.Println(x[0]) //print 116
    fmt.Printf("%T",x[0]) //prints uint8
}

如果需要在 UTF8 类型的 String 中取出指定字符,那么需要用到 unicode/utf8 与实验性的 utf8string 包。utf8string 包包含 AT() 方法,可以取出字符,也可以将 String 转换为 Rune SLice。

String并不一定是UTF8格式
级别:新手入门级

String 类型不一定是 UTF8 格式,String 中也可以包含自定义的文字/字节。只有需要将字符串显示出来的时候才需要用 UTF8 格式,其他情况下可以随便用转义来表示任意字符。

可以使用 unicode/utf8 包中体重的 ValidString() 方法判断是否是 UTF8 类型的文本。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data1 := "ABC"
    fmt.Println(utf8.ValidString(data1)) //prints: true

    data2 := "A\xfeC"
    fmt.Println(utf8.ValidString(data2)) //prints: false
}

String 长度
级别:新手入门级

Python 开发者的代码:

data = u’♥’
print(len(data)) #prints: 1
转换成类似的 Go 代码如下:

package main

import "fmt"

func main() {  
    data := "♥"
    fmt.Println(len(data)) //prints: 3
}
Go 的 len() 方法和 Python 的并不相同,和 Python 的 len 方法等价的 Go 方法是 RuneCountInString。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data := "♥"
    fmt.Println(utf8.RuneCountInString(data)) //prints: 1
}

当然有些情况(例如法语)的情况,RuneCountInString 也并不能完全返回字符数目,因为有些字符是使用多个字符的方式进行存储的。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data := "é"
    fmt.Println(len(data))                    //prints: 3
    fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}

多行的 Slice Arry 和 Map 赋值中缺少逗号
Missing Comma In Multi-Line Slice, Array, and Map Literals

级别:新手入门级 错误信息:

package main

func main() {  
    x := []int{
    1,
    2 //error
    }
    _ = x
}

Compile Errors:

/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }
修正代码:

package main

func main() {  
    x := []int{
    1,
    2,
    }
    x = x

    y := []int{3,4,} //no error
    y = y
}

当然,如果把这些东西写在一行中,可以不加最后的逗号。

log.Fatal 与 log.Panic 在后台悄悄做了一些事情
级别:新手入门级

日志库提供了不同级别的日志记录,如果使用 Fatal 和 Panic 级别的日志,那么记录完这条日志后,应用程序便会退出而不会继续执行。


package main

import "log"

func main() {  
    log.Fatalln("Fatal Level: log entry") //app exits here
    log.Println("Normal Level: log entry")
}

内置的数据结构操作是无锁的
级别:新手入门级

虽然 Go 语言天生高并发,但是 Go 并没有考虑到数据安全。为了实现“原子化”的数据操作,开发者需要自己对数据操作进行加锁。当然, goroutine 和 channel 以及 sync 都是手动加锁的好方案。

文档更新时间: 2019-11-25 16:02   作者:月影鹏鹏