最近准备写一些关于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()
函数:
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)
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("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*()
时能做更多日志外的事,如中断程序的执行等:
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 代码块。
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 特立独行,去掉了前置操作,同时
、—
只作为运算符而非表达式。
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 操作符来按位取反:
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。
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 操作时,这些私有字段会被忽略,导出时得到零值:
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:
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
变量并不会受到影响。
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 接收数据是安全的:
接收状态值 ok
是 false
时表明 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
的结果是 {}
:
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