Golang 需要避免踩的 50 个坑(二)

最近准备写一些关于golang的技术博文,本文是之前在GitHub上看到的golang技术译文,感觉很有帮助,先给各位读者分享一下。

前言

Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。

如果花时间学习官方 doc、wiki、讨论邮件列表、 Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,新手跳过这些坑,能减少大量调试代码的时间。

初级篇:1-35(二)

18. string 与索引操作符

对字符串用索引访问返回的不是字符,而是一个 byte 值。

这种处理方式和其他语言一样,比如 PHP 中:

1> php -r '$name="中文"; var_dump($name);'    # "中文" 占用 6 个字节
2string(6) "中文"
3
4>
 php -r '$name="中文"; var_dump($name[0]);' # 把第一个字节当做 Unicode 字符读取,显示 U+FFFD
5string(1) "�"    
6
7>
 php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
8string(3) "中"
1func main() {
2    x := "ascii"
3    fmt.Println(x[0])       // 97
4    fmt.Printf("%T\n", x[0])// uint8
5}

如果需要使用 for range 迭代访问字符串中的字符(unicode code point / rune),标准库中有 "unicode/utf8" 包来做 UTF8 的相关解码编码。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的库函数。

19. 字符串并不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。

判断字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函数:

 1func main() {
2    str1 := "ABC"
3    fmt.Println(utf8.ValidString(str1)) // true
4
5    str2 := "A\xfeC"
6    fmt.Println(utf8.ValidString(str2)) // false
7
8    str3 := "A\\xfeC"
9    fmt.Println(utf8.ValidString(str3)) // true // 把转义字符转义成字面值
10}

20. 字符串的长度

在 Python 中:

1data = u'♥'  
2print(len(data)) # 1

然而在 Go 中:

1func main() {
2    char := "♥"
3    fmt.Println(len(char))  // 3
4}

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

如果要得到字符串的字符数,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

1func main() {
2    char := "♥"
3    fmt.Println(utf8.RuneCountInString(char))   // 1
4}

注意: RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:

1func main() {
2    char := "é"
3    fmt.Println(len(char))  // 3
4    fmt.Println(utf8.RuneCountInString(char))   // 2
5    fmt.Println("cafe\u0301")   // café // 法文的 cafe,实际上是两个 rune 的组合
6}

参考:normalization

21. 在多行 array、slice、map 语句中缺少 `,` 号

1func main() {
2    x := []int {
3        1,
4        2   // syntax error: unexpected newline, expecting comma or }
5    }
6    y := []int{1,2,}    
7    z := []int{1,2
8    // ...
9}

声明语句中 } 折叠到单行后,尾部的 , 不是必需的。

22. `log.Fatal` 和 `log.Panic` 不只是 log

log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()Panic*() 时能做更多日志外的事,如中断程序的执行等:

1func main() {
2    log.Fatal("Fatal level log: log entry")     // 输出信息后,程序终止执行
3    log.Println("Nomal level log: log entry")
4}

23. 对内建数据结构的操作并不是同步的

尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

goroutine 和 channel 是进行原子操作的好方法,或使用 "sync" 包中的锁。

24. range 迭代 string 得到的值

range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。

注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。

 1func main() {
2    data := "A\xfe\x02\xff\x04"
3    for _, v := range data {
4        fmt.Printf("%#x ", v)   // 0x41 0xfffd 0x2 0xfffd 0x4   // 错误
5    }
6
7    for _, v := range []byte(data) {
8        fmt.Printf("%#x ", v)   // 0x41 0xfe 0x2 0xff 0x4   // 正确
9    }
10}

25. range 迭代 map

如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:

1func main() {
2    m := map[string]int{"one"1"two"2"three"3"four"4}
3    for k, v := range m {
4        fmt.Println(k, v)
5    }
6}

如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:

26. switch 中的 fallthrough 语句

switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

 1func main() {
2    isSpace := func(char byte) bool {
3        switch char {
4        case ' ':   // 空格符会直接 break,返回 false // 和其他语言不一样
5        // fallthrough  // 返回 true
6        case '\t':
7            return true
8        }
9        return false
10    }
11    fmt.Println(isSpace('\t'))  // true
12    fmt.Println(isSpace(' '))   // false
13}

不过你可以在 case 代码块末尾使用 fallthrough,强制执行下一个 case 代码块。

也可以改写 case 为多条件判断:

 1func main() {
2    isSpace := func(char byte) bool {
3        switch char {
4        case ' ''\t':
5            return true
6        }
7        return false
8    }
9    fmt.Println(isSpace('\t'))  // true
10    fmt.Println(isSpace(' '))   // true
11}

27. 自增和自减运算

很多编程语言都自带前置后置的 ++-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++ 只作为运算符而非表达式。

 1// 错误示例
2func main() {
3    data := []int{123}
4    i := 0
5    ++i         // syntax error: unexpected ++, expecting }
6    fmt.Println(data[i++])  // syntax error: unexpected ++, expecting :
7}
8
9
10// 正确示例
11func main() {
12    data := []int{123}
13    i := 0
14    i++
15    fmt.Println(data[i])    // 2
16}

28. 按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:

 1// 错误的取反操作
2func main() {
3    fmt.Println(~2)     // bitwise complement operator is ^
4}
5
6
7// 正确示例
8func main() {
9    var d uint8 = 2
10    fmt.Printf("%08b\n", d)     // 00000010
11    fmt.Printf("%08b\n", ^d)    // 11111101
12}

同时 ^ 也是按位异或(XOR)操作符。

一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02,与二元的 XOR 操作 0x22 XOR 0xff 是一致的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

 1func main() {
2    var a uint8 = 0x82
3    var b uint8 = 0x02
4    fmt.Printf("%08b [A]\n", a)
5    fmt.Printf("%08b [B]\n", b)
6
7    fmt.Printf("%08b (NOT B)\n", ^b)
8    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)
9
10    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
11    fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
12    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
13    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
14}
110000010 [A]
200000010 [B]
311111101 (NOT B)
400000010 ^ 11111111 = 11111101 [B XOR 0xff]
510000010 ^ 00000010 = 10000000 [A XOR B]
610000010 & 00000010 = 00000010 [A AND B]
710000010 &^00000010 = 10000000 [A 'AND NOT' B]
810000010&(^00000010)= 10000000 [A AND (NOT B)]

29. 运算符的优先级

除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

 1func main() {
2    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n"0x2&0x2+0x4// & 优先 +
3    //prints: 0x2 & 0x2 + 0x4 -> 0x6
4    //Go:    (0x2 & 0x2) + 0x4
5    //C++:    0x2 & (0x2 + 0x4) -> 0x2
6
7    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n"0x2+0x2<<0x1)   // << 优先 +
8    //prints: 0x2 + 0x2 << 0x1 -> 0x6
9    //Go:     0x2 + (0x2 << 0x1)
10    //C++:   (0x2 + 0x2) << 0x1 -> 0x8
11
12    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n"0xf|0x2^0x2// | 优先 ^
13    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
14    //Go:    (0xf | 0x2) ^ 0x2
15    //C++:    0xf | (0x2 ^ 0x2) -> 0xf
16}

优先级列表:

1Precedence    Operator
2    5             *  /  %  <<  >>  &  &^
3    4             +  -  |  ^
4    3             ==  !=  <  <=  >  >=
5    2             &&
6    1             ||

30. 不导出的 struct 字段无法被 encode

以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

 1func main() {
2    in := MyData{1"two"}
3    fmt.Printf("%#v\n", in) // main.MyData{One:1, two:"two"}
4
5    encoded, _ := json.Marshal(in)
6    fmt.Println(string(encoded))    // {"One":1}    // 私有字段 two 被忽略了
7
8    var out MyData
9    json.Unmarshal(encoded, &out)
10    fmt.Printf("%#v\n", out)    // main.MyData{One:1, two:""}
11}

31. 程序退出时还有 goroutine 在执行

程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:

 1// 主程序会直接退出
2func main() {
3    workerCount := 2
4    for i := 0; i < workerCount; i++ {
5        go doIt(i)
6    }
7    time.Sleep(1 * time.Second)
8    fmt.Println("all done!")
9}
10
11func doIt(workerID int) {
12    fmt.Printf("[%v] is running\n", workerID)
13    time.Sleep(3 * time.Second)     // 模拟 goroutine 正在执行 
14    fmt.Printf("[%v] is done\n", workerID)
15}

如下,main() 主程序不等两个 goroutine 执行完就直接退出了:

常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:

 1// 等待所有 goroutine 执行完毕
2// 进入死锁
3func main() {
4    var wg sync.WaitGroup
5    done := make(chan struct{})
6
7    workerCount := 2
8    for i := 0; i < workerCount; i++ {
9        wg.Add(1)
10        go doIt(i, done, wg)
11    }
12
13    close(done)
14    wg.Wait()
15    fmt.Println("all done!")
16}
17
18func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
19    fmt.Printf("[%v] is running\n", workerID)
20    defer wg.Done()
21    <-done
22    fmt.Printf("[%v] is done\n", workerID)
23}

执行结果:

看起来好像 goroutine 都执行完了,然而报错:

fatal error: all goroutines are asleep - deadlock!

为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

原因是 goroutine 得到的 "WaitGroup" 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。

 1// 等待所有 goroutine 执行完毕
2// 使用传址方式为 WaitGroup 变量传参
3// 使用 channel 关闭 goroutine
4
5func main() {
6    var wg sync.WaitGroup
7    done := make(chan struct{})
8    ch := make(chan interface{})
9
10    workerCount := 2
11    for i := 0; i < workerCount; i++ {
12        wg.Add(1)
13        go doIt(i, ch, done, &wg)    // wg 传指针,doIt() 内部会改变 wg 的值
14    }
15
16    for i := 0; i < workerCount; i++ {  // 向 ch 中发送数据,关闭 goroutine
17        ch <- i
18    }
19
20    close(done)
21    wg.Wait()
22    close(ch)
23    fmt.Println("all done!")
24}
25
26func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
27    fmt.Printf("[%v] is running\n", workerID)
28    defer wg.Done()
29    for {
30        select {
31        case m := <-ch:
32            fmt.Printf("[%v] m => %v\n", workerID, m)
33        case <-done:
34            fmt.Printf("[%v] is done\n", workerID)
35            return
36        }
37    }
38}

运行效果:

32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

 1func main() {
2    ch := make(chan string)
3
4    go func() {
5        for m := range ch {
6            fmt.Println("Processed:", m)
7            time.Sleep(1 * time.Second) // 模拟需要长时间运行的操作
8        }
9    }()
10
11    ch <- "cmd.1"
12    ch <- "cmd.2" // 不会被接收处理
13}

运行效果:

33. 向已关闭的 channel 发送数据会造成 panic

从已关闭的 channel 接收数据是安全的:

接收状态值 ok 是 false 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false

向已关闭的 channel 中发送数据会造成 panic:

 1func main() {
2    ch := make(chan int)
3    for i := 0; i < 3; i++ {
4        go func(idx int) {
5            ch <- idx
6        }(i)
7    }
8
9    fmt.Println(<-ch)       // 输出第一个发送的值
10    close(ch)           // 不能关闭,还有其他的 sender
11    time.Sleep(2 * time.Second) // 模拟做其他的操作
12}

运行结果:

针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done 的结果是 {}

 1func main() {
2    ch := make(chan int)
3    done := make(chan struct{})
4
5    for i := 0; i < 3; i++ {
6        go func(idx int) {
7            select {
8            case ch <- (idx + 1) * 2:
9                fmt.Println(idx, "Send result")
10            case <-done:
11                fmt.Println(idx, "Exiting")
12            }
13        }(i)
14    }
15
16    fmt.Println("Result: ", <-ch)
17    close(done)
18    time.Sleep(3 * time.Second)
19}

运行效果:

34. 使用了值为 `nil ` 的 channel

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

 1func main() {
2    var ch chan int // 未初始化,值为 nil
3    for i := 0; i < 3; i++ {
4        go func(i int) {
5            ch <- i
6        }(i)
7    }
8
9    fmt.Println("Result: ", <-ch)
10    time.Sleep(2 * time.Second)
11}

runtime 死锁错误:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]

利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:

 1func main() {
2    inCh := make(chan int)
3    outCh := make(chan int)
4
5    go func() {
6        var in <-chan int = inCh
7        var out chan<- int
8        var val int
9
10        for {
11            select {
12            case out <- val:
13                println("--------")
14                out = nil
15                in = inCh
16            case val = <-in:
17                println("++++++++++")
18                out = outCh
19                in = nil
20            }
21        }
22    }()
23
24    go func() {
25        for r := range outCh {
26            fmt.Println("Result: ", r)
27        }
28    }()
29
30    time.Sleep(0)
31    inCh <- 1
32    inCh <- 2
33    time.Sleep(3 * time.Second)
34}

运行效果:

35. 若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值:

 1type data struct {
2    num   int
3    key   *string
4    items map[string]bool
5}
6
7func (this *data) pointerFunc() {
8    this.num = 7
9}
10
11func (this data) valueFunc() {
12    this.num = 8
13    *this.key = "valueFunc.key"
14    this.items["valueFunc"] = true
15}
16
17func main() {
18    key := "key1"
19
20    d := data{1, &key, make(map[string]bool)}
21    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
22
23    d.pointerFunc() // 修改 num 的值为 7
24    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
25
26    d.valueFunc()   // 修改 key 和 items 的值
27    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
28}

运行结果:

系列文章

Golang 需要避免踩的 50 个坑

本文转载自https://github.com/wuYin/blog/blob/master/50-shades-of-golang-traps-gotchas-mistakes.md