最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。
前言Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。
如果花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量調試代碼的時間。
初級篇:1-35(二)18. string 與索引操作符對字符串用索引訪問返回的不是字符,而是一個 byte 值。
這種處理方式和其他語言一樣,比如 PHP 中:
1> php -r '$name="中文"; var_dump($name);'
2string(6) "中文"
3
4> php -r '$name="中文"; var_dump($name[0]);'
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])
4 fmt.Printf("%T\n", x[0])
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))
4
5 str2 := "A\xfeC"
6 fmt.Println(utf8.ValidString(str2))
7
8 str3 := "A\\xfeC"
9 fmt.Println(utf8.ValidString(str3))
10}
在 Python 中:
1data = u'♥'
2print(len(data))
然而在 Go 中:
1func main() {
2 char := "♥"
3 fmt.Println(len(char))
4}
Go 的內建函數 len() 返回的是字符串的 byte 數量,而不是像 Python 中那樣是計算 Unicode 字符數。
如果要得到字符串的字符數,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)
1func main() {
2 char := "♥"
3 fmt.Println(utf8.RuneCountInString(char))
4}
注意: RuneCountInString 並不總是返回我們看到的字符數,因為有的字符會佔用 2 個 rune:
1func main() {
2 char := "é"
3 fmt.Println(len(char))
4 fmt.Println(utf8.RuneCountInString(char))
5 fmt.Println("cafe\u0301")
6}
參考:normalization
21. 在多行 array、slice、map 語句中缺少 `,` 號1func main() {
2 x := []int {
3 1,
4 2
5 }
6 y := []int{1,2,}
7 z := []int{1,2}
8
9}
聲明語句中 } 摺疊到單行後,尾部的 , 不是必需的。
22. `log.Fatal` 和 `log.Panic` 不只是 loglog 標準庫提供了不同的日誌記錄等級,與其他語言的日誌庫不同,Go 的 log 包在調用 Fatal*()、Panic*() 時能做更多日誌外的事,如中斷程序的執行等:
1func main() {
2 log.Fatal("Fatal level log: log entry")
3 log.Println("Nomal level log: log entry")
4}
儘管 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)
5 }
6
7 for _, v := range []byte(data) {
8 fmt.Printf("%#x ", v)
9 }
10}
如果你希望以特定的順序(如按 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 ' ':
5
6 case '\t':
7 return true
8 }
9 return false
10 }
11 fmt.Println(isSpace('\t'))
12 fmt.Println(isSpace(' '))
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'))
10 fmt.Println(isSpace(' '))
11}
很多程式語言都自帶前置後置的 ++、-- 運算。但 Go 特立獨行,去掉了前置操作,同時 ++、— 只作為運算符而非表達式。
1
2func main() {
3 data := []int{1, 2, 3}
4 i := 0
5 ++i
6 fmt.Println(data[i++])
7}
8
9
10
11func main() {
12 data := []int{1, 2, 3}
13 i := 0
14 i++
15 fmt.Println(data[i])
16}
很多程式語言使用 ~ 作為一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符來按位取反:
1
2func main() {
3 fmt.Println(~2)
4}
5
6
7
8func main() {
9 var d uint8 = 2
10 fmt.Printf("%08b\n", d)
11 fmt.Printf("%08b\n", ^d)
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)]
除了位清除(bit clear)操作符,Go 也有很多和其他語言一樣的位操作符,但優先級另當別論。
1func main() {
2 fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4)
3
4
5
6
7 fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1)
8
9
10
11
12 fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2)
13
14
15
16}
優先級列表:
1Precedence Operator
2 5 * / % << >> & &^
3 4 + - | ^
4 3 == != < <= > >=
5 2 &&
6 1 ||
以小寫字母開頭的欄位成員是無法被外部直接訪問的,所以 struct 在進行 json、xml、gob 等格式的 encode 操作時,這些私有欄位會被忽略,導出時得到零值:
1func main() {
2 in := MyData{1, "two"}
3 fmt.Printf("%#v\n", in)
4
5 encoded, _ := json.Marshal(in)
6 fmt.Println(string(encoded))
7
8 var out MyData
9 json.Unmarshal(encoded, &out)
10 fmt.Printf("%#v\n", out)
11}
程序默認不等所有 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)
14 fmt.Printf("[%v] is done\n", workerID)
15}
如下,main() 主程序不等兩個 goroutine 執行完就直接退出了:
常用解決辦法:使用 "WaitGroup" 變量,它會讓主程序等待所有 goroutine 執行完畢再退出。
如果你的 goroutine 要做消息的循環處理等耗時操作,可以向它們發送一條 kill 消息來關閉它們。或直接關閉一個它們都等待接收數據的 channel:
1
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
2
3
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)
14 }
15
16 for i := 0; i < workerCount; i++ {
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)
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
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()
24 fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
25
26 d.valueFunc()
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