前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 需要避免踩的 50 个坑(二)

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

作者头像
aoho求索
发布2019-03-07 10:41:18
1.1K0
发布2019-03-07 10:41:18
举报
文章被收录于专栏:aoho求索aoho求索

最近准备写一些关于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("%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() 函数:

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

代码语言: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("cafe\u0301")   // 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 := "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 个迭代结果也是可能的,如:

代码语言: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("%08b\n", d)     // 00000010
11    fmt.Printf("%08b\n", ^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("%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}
代码语言: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 -> %#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}

优先级列表:

代码语言: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("%#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 都执行完才退出,这点需要特别注意:

代码语言: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 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:

代码语言: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 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 变量并不会受到影响。

代码语言: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 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 可能没有足够的时间处理下一个数据。如:

代码语言: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=%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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-02-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 aoho求索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 初级篇:1-35(二)
    • 18. string 与索引操作符
      • 19. 字符串并不都是 UTF8 文本
        • 20. 字符串的长度
          • 21. 在多行 array、slice、map 语句中缺少 `,` 号
            • 22. `log.Fatal` 和 `log.Panic` 不只是 log
              • 23. 对内建数据结构的操作并不是同步的
                • 24. range 迭代 string 得到的值
                  • 25. range 迭代 map
                    • 26. switch 中的 fallthrough 语句
                      • 27. 自增和自减运算
                        • 28. 按位取反
                          • 29. 运算符的优先级
                            • 30. 不导出的 struct 字段无法被 encode
                              • 31. 程序退出时还有 goroutine 在执行
                                • 32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回
                                  • 33. 向已关闭的 channel 发送数据会造成 panic
                                    • 34. 使用了值为 `nil ` 的 channel
                                      • 35. 若函数 receiver 传参是传值方式,则无法修改参数的原有值
                                        • 系列文章
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档