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

2019-03-07 10:41:18 浏览数 (1)

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

前言

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

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

初级篇:1-35(二)

18. string 与索引操作符

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

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

代码语言:javascript复制
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) "中"
代码语言:javascript复制
1func main() {
2    x := "ascii"
3    fmt.Println(x[0])       // 97
4    fmt.Printf("%Tn", 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() 函数:

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

20. 字符串的长度

在 Python 中:

代码语言:javascript复制
1data = u'♥'  
2print(len(data)) # 1

然而在 Go 中:

代码语言:javascript复制
1func main() {
2    char := "♥"
3    fmt.Println(len(char))  // 3
4}

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

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

代码语言:javascript复制
1func main() {
2    char := "♥"
3    fmt.Println(utf8.RuneCountInString(char))   // 1
4}

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

代码语言:javascript复制
1func main() {
2    char := "é"
3    fmt.Println(len(char))  // 3
4    fmt.Println(utf8.RuneCountInString(char))   // 2
5    fmt.Println("cafeu0301")   // café // 法文的 cafe,实际上是两个 rune 的组合
6}

参考:normalization

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

代码语言:javascript复制
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*() 时能做更多日志外的事,如中断程序的执行等:

代码语言:javascript复制
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 再进行操作。

代码语言:javascript复制
 1func main() {
 2    data := "Axfex02xffx04"
 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 个迭代结果也是可能的,如:

代码语言:javascript复制
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 代码块。

代码语言:javascript复制
 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 为多条件判断:

代码语言:javascript复制
 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 特立独行,去掉了前置操作,同时 只作为运算符而非表达式。

代码语言:javascript复制
 1// 错误示例
 2func main() {
 3    data := []int{1, 2, 3}
 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{1, 2, 3}
13    i := 0
14    i  
15    fmt.Println(data[i])    // 2
16}

28. 按位取反

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

代码语言:javascript复制
 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("bn", d)     // 00000010
11    fmt.Printf("bn", ^d)    // 11111101
12}

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

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

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

代码语言:javascript复制
 1func main() {
 2    var a uint8 = 0x82
 3    var b uint8 = 0x02
 4    fmt.Printf("b [A]n", a)
 5    fmt.Printf("b [B]n", b)
 6
 7    fmt.Printf("b (NOT B)n", ^b)
 8    fmt.Printf("b ^ b = b [B XOR 0xff]n", b, 0xff, b^0xff)
 9
10    fmt.Printf("b ^ b = b [A XOR B]n", a, b, a^b)
11    fmt.Printf("b & b = b [A AND B]n", a, b, a&b)
12    fmt.Printf("b &^b = b [A 'AND NOT' B]n", a, b, a&^b)
13    fmt.Printf("b&(^b)= b [A AND (NOT B)]n", a, b, a&(^b))
14}
代码语言:javascript复制
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 也有很多和其他语言一样的位操作符,但优先级另当别论。

代码语言:javascript复制
 1func main() {
 2    fmt.Printf("0x2 & 0x2   0x4 -> %#xn", 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 -> %#xn", 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 -> %#xn", 0xf|0x2^0x2) // | 优先 ^
13    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
14    //Go:    (0xf | 0x2) ^ 0x2
15    //C  :    0xf | (0x2 ^ 0x2) -> 0xf
16}

优先级列表:

代码语言:javascript复制
1Precedence    Operator
2    5             *  /  %  <<  >>  &  &^
3    4                -  |  ^
4    3             ==  !=  <  <=  >  >=
5    2             &&
6    1             ||

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

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

代码语言:javascript复制
 1func main() {
 2    in := MyData{1, "two"}
 3    fmt.Printf("%#vn", 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("%#vn", out)    // main.MyData{One:1, two:""}
11}

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

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

代码语言:javascript复制
 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 runningn", workerID)
13    time.Sleep(3 * time.Second)     // 模拟 goroutine 正在执行 
14    fmt.Printf("[%v] is donen", workerID)
15}

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

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

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

代码语言:javascript复制
 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 runningn", workerID)
20    defer wg.Done()
21    <-done
22    fmt.Printf("[%v] is donen", workerID)
23}

执行结果:

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

fatal error: all goroutines are asleep - deadlock!

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

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

代码语言:javascript复制
 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 runningn", workerID)
28    defer wg.Done()
29    for {
30        select {
31        case m := <-ch:
32            fmt.Printf("[%v] m => %vn", workerID, m)
33        case <-done:
34            fmt.Printf("[%v] is donen", workerID)
35            return
36        }
37    }
38}

运行效果:

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

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

代码语言:javascript复制
 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 接收数据是安全的:

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

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

代码语言:javascript复制
 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 的结果是 {}

代码语言:javascript复制
 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 上发送和接收数据将永久阻塞:

代码语言:javascript复制
 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 语句块:

代码语言:javascript复制
 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 中的元素的,才会更新原有值:

代码语言:javascript复制
 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=%vn", d.num, *d.key, d.items)
22
23    d.pointerFunc() // 修改 num 的值为 7
24    fmt.Printf("num=%v  key=%v  items=%vn", d.num, *d.key, d.items)
25
26    d.valueFunc()   // 修改 key 和 items 的值
27    fmt.Printf("num=%v  key=%v  items=%vn", d.num, *d.key, d.items)
28}

运行结果:

系列文章

Golang 需要避免踩的 50 个坑

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

0 人点赞