Golang 需要避免踩的 50 個坑(一)
最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。
前言
Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。
如果花時間學習官方 doc、wiki、討論郵件列表 、Rob Pike 的大量文章以及 Go 的原始碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量除錯程式碼的時間。
初級篇:1-35
1. 左大括號{
一般不能單獨放一行
在其他大多數語言中,{
的位置你自行決定。Go 比較特別,遵守分號注入規則(automatic semicolon injection):編譯器會在每行程式碼尾部特定分隔符後加;
來分隔多條語句,比如會在)
後加分號:
// 錯誤示例 func main() { println("hello world") } // 等效於 func main();// 無函式體 { println("hello world") }
./main.go: missing function body ./main.go: syntax error: unexpected semicolon or newline before {
// 正確示例 func main() { println("hello world") }
注意程式碼塊等特殊情況:
// { 並不遵守分號注入規則,不會在其後邊自動加分,此時可換行 func main() { { println("hello world") } }
2. 未使用的變數
如果在函式體程式碼中有未使用的變數,則無法通過編譯,不過全域性變數宣告但不使用是可以的。
即使變數聲明後為變數賦值,依舊無法通過編譯,需在某處使用它:
// 錯誤示例 var gvar int // 全域性變數,宣告不使用也可以 func main() { var one int // error: one declared and not used two := 2// error: two declared and not used var three int// error: three declared and not used three = 3 } // 正確示例 // 可以直接註釋或移除未使用的變數 func main() { var one int _ = one two := 2 println(two) var three int one = three var four int four = four }
3. 未使用的 import
如果你 import 一個包,但包中的變數、函式、介面和結構體一個都沒有用到的話,將編譯失敗。
可以使用_
下劃線符號作為別名來忽略匯入的包,從而避免編譯錯誤,這隻會執行 package 的init()
// 錯誤示例 import ( "fmt"// imported and not used: "fmt" "log"// imported and not used: "log" "time"// imported and not used: "time" ) func main() { } // 正確示例 // 可以使用 goimports 工具來註釋或移除未使用到的包 import ( _ "fmt" "log" "time" ) func main() { _ = log.Println _ = time.Now }
4. 簡短宣告的變數只能在函式內部使用
// 錯誤示例 myvar := 1// syntax error: non-declaration statement outside function body func main() { } // 正確示例 varmyvar = 1 func main() { }
5. 使用簡短宣告來重複宣告變數
不能用簡短宣告方式來單獨為一個變數重複宣告,:=
左側至少有一個新變數,才允許多變數的重複宣告:
// 錯誤示例 func main() { one := 0 one := 1 // error: no new variables on left side of := } // 正確示例 func main() { one := 0 one, two := 1, 2// two 是新變數,允許 one 的重複宣告。比如 error 處理經常用同名變數 err one, two = two, one// 交換兩個變數值的簡寫 }
6. 不能使用簡短宣告來設定欄位的值
struct 的變數欄位不能使用:=
來賦值以使用預定義的變數來避免解決:
// 錯誤示例 type info struct { result int } func work() (int, error) { return 3, nil } func main() { var data info data.result, err := work()// error: non-name data.result on left side of := fmt.Printf("info: %+v\n", data) } // 正確示例 func main() { var data info var err error// err 需要預宣告 data.result, err = work() if err != nil { fmt.Println(err) return } fmt.Printf("info: %+v\n", data) }
7. 不小心覆蓋了變數
對從動態語言轉過來的開發者來說,簡短宣告很好用,這可能會讓人誤會:=
是一個賦值操作符。
如果你在新的程式碼塊中像下邊這樣誤用了:=
,編譯不會報錯,但是變數不會按你的預期工作:
func main() { x := 1 println(x)// 1 { println(x)// 1 x := 2 println(x)// 2// 新的 x 變數的作用域只在程式碼塊內部 } println(x)// 1 }
這是 Go 開發者常犯的錯,而且不易被發現。
可使用vet
工具來診斷這種變數覆蓋,Go 預設不做覆蓋檢查,新增-shadow
選項來啟用:
> go tool vet -shadow main.go main.go:9: declaration of "x" shadows declaration at main.go:5
注意 vet 不會報告全部被覆蓋的變數,可以使用go-nyet 來做進一步的檢測:
> $GOPATH/bin/go-nyet main.go main.go:10:3:Shadowing variable `x`
8. 顯式型別的變數無法使用 nil 來初始化
nil
是 interface、function、pointer、map、slice 和 channel 型別變數的預設初始值。但宣告時不指定型別,編譯器也無法推斷出變數的具體型別。
// 錯誤示例 func main() { var x = nil// error: use of untyped nil _ = x } // 正確示例 func main() { var x interface{} = nil _ = x }
9. 直接使用值為 nil 的 slice、map
允許對值為 nil 的 slice 新增元素,但對值為 nil 的 map 新增元素則會造成執行時 panic
// map 錯誤示例 func main() { var m map[string]int m["one"] = 1// error: panic: assignment to entry in nil map // m := make(map[string]int)// map 的正確宣告,分配了實際的記憶體 } // slice 正確示例 func main() { var s []int s = append(s, 1) }
10. map 容量
在建立 map 型別的變數時可以指定容量,但不能像 slice 一樣使用cap()
來檢測分配空間的大小:
// 錯誤示例 func main() { m := make(map[string]int, 99) println(cap(m)) // error: invalid argument m1 (type map[string]int) for cap }
11.string 型別的變數值不能為 nil
對那些喜歡用nil
初始化字串的人來說,這就是坑:
// 錯誤示例 func main() { var s string = nil// cannot use nil as type string in assignment if s == nil {// invalid operation: s == nil (mismatched types string and nil) s = "default" } } // 正確示例 func main() { var s string// 字串型別的零值是空串 "" if s == "" { s = "default" } }
12. Array 型別的值作為函式引數
在 C/C++ 中,陣列(名)是指標。將陣列作為引數傳進函式時,相當於傳遞了陣列記憶體地址的引用,在函式內部會改變該陣列的值。
在 Go 中,陣列是值。作為引數傳進函式時,傳遞的是陣列的原始值拷貝,此時在函式內部是無法更新該陣列的:
// 陣列使用值拷貝傳參 func main() { x := [3]int{1,2,3} func(arr [3]int) { arr[0] = 7 fmt.Println(arr)// [7 2 3] }(x) fmt.Println(x)// [1 2 3]// 並不是你以為的 [7 2 3] }
如果想修改引數陣列:
- 直接傳遞指向這個陣列的指標型別:
// 傳址會修改原資料 func main() { x := [3]int{1,2,3} func(arr *[3]int) { (*arr)[0] = 7 fmt.Println(arr)// &[7 2 3] }(&x) fmt.Println(x)// [7 2 3] }
- 直接使用 slice:即使函式內部得到的是 slice 的值拷貝,但依舊會更新 slice 的原始資料(底層 array)
// 會修改 slice 的底層 array,從而修改 slice func main() { x := []int{1, 2, 3} func(arr []int) { arr[0] = 7 fmt.Println(x)// [7 2 3] }(x) fmt.Println(x)// [7 2 3] }
13. range 遍歷 slice 和 array 時混淆了返回值
與其他程式語言中的for-in
、foreach
遍歷語句不同,Go 中的range
在遍歷時會生成 2 個值,第一個是元素索引,第二個是元素的值:
// 錯誤示例 func main() { x := []string{"a", "b", "c"} for v := range x { fmt.Println(v)// 1 2 3 } } // 正確示例 func main() { x := []string{"a", "b", "c"} for _, v := range x {// 使用 _ 丟棄索引 fmt.Println(v) } }
14. slice 和 array 其實是一維資料
看起來 Go 支援多維的 array 和 slice,可以建立陣列的陣列、切片的切片,但其實並不是。
對依賴動態計算多維陣列值的應用來說,就效能和複雜度而言,用 Go 實現的效果並不理想。
可以使用原始的一維陣列、“獨立“ 的切片、“共享底層陣列”的切片來建立動態的多維陣列。
-
使用原始的一維陣列:要做好索引檢查、溢位檢測、以及當陣列滿時再新增值時要重新做記憶體分配。
-
使用“獨立”的切片分兩步:
- 建立外部 slice
-
對每個內部 slice 進行記憶體分配
注意內部的 slice 相互獨立,使得任一內部 slice 增縮都不會影響到其他的 slice
// 使用各自獨立的 6 個 slice 來建立 [2][3] 的動態多維陣列 func main() { x := 2 y := 4 table := make([][]int, x) for i:= range table { table[i] = make([]int, y) } }
- 使用“共享底層陣列”的切片
- 建立一個存放原始資料的容器 slice
- 建立其他的 slice
- 切割原始 slice 來初始化其他的 slice
func main() { h, w := 2, 4 raw := make([]int, h*w) for i := range raw { raw[i] = i } // 初始化原始 slice fmt.Println(raw, &raw[4])// [0 1 2 3 4 5 6 7] 0xc420012120 table := make([][]int, h) for i := range table { // 等間距切割原始 slice,建立動態多維陣列 table // 0: raw[0*4: 0*4 + 4] // 1: raw[1*4: 1*4 + 4] table[i] = raw[i*w : i*w + w] } fmt.Println(table, &table[1][0])// [[0 1 2 3] [4 5 6 7]] 0xc420012120 }
更多關於多維陣列的參考
go-how-is-two-dimensional-arrays-memory-representation
what-is-a-concise-way-to-create-a-2d-slice-in-go
15. 訪問 map 中不存在的 key
和其他程式語言類似,如果訪問了 map 中不存在的 key 則希望能返回 nil,比如在 PHP 中:
> php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);' NULL
Go 則會返回元素對應資料型別的零值,比如nil
、''
、false
和 0,取值操作總有值返回,故不能通過取出來的值來判斷 key 是不是在 map 中。
檢查 key 是否存在可以用 map 直接訪問,檢查返回的第二個引數即可:
// 錯誤的 key 檢測方式 func main() { x := map[string]string{"one": "2", "two": "", "three": "3"} if v := x["two"]; v == "" { fmt.Println("key two is no entry")// 鍵 two 存不存在都會返回的空字串 } } // 正確示例 func main() { x := map[string]string{"one": "2", "two": "", "three": "3"} if _, ok := x["two"]; !ok { fmt.Println("key two is no entry") } }
16. string 型別的值是常量,不可更改
嘗試使用索引遍歷字串,來更新字串中的個別字元,是不允許的。
string 型別的值是隻讀的二進位制 byte slice,如果真要修改字串中的字元,將 string 轉為 []byte 修改後,再轉為 string 即可:
// 修改字串的錯誤示例 func main() { x := "text" x[0] = "T"// error: cannot assign to x[0] fmt.Println(x) } // 修改示例 func main() { x := "text" xBytes := []byte(x) xBytes[0] = 'T'// 注意此時的 T 是 rune 型別 x = string(xBytes) fmt.Println(x)// Text }
注意:上邊的示例並不是更新字串的正確姿勢,因為一個 UTF8 編碼的字元可能會佔多個位元組,比如漢字就需要 3~4 個位元組來儲存,此時更新其中的一個位元組是錯誤的。
更新字串的正確姿勢:將 string 轉為 rune slice(此時 1 個 rune 可能佔多個 byte),直接更新 rune 中的字元
func main() { x := "text" xRunes := []rune(x) xRunes[0] = '我' x = string(xRunes) fmt.Println(x)// 我ext }
17. string 與 byte slice 之間的轉換
當進行 string 和 byte slice 相互轉換時,參與轉換的是拷貝的原始值。這種轉換的過程,與其他程式設計語的強制型別轉換操作不同,也和新 slice 與舊 slice 共享底層陣列不同。
Go 在 string 與 byte slice 相互轉換上優化了兩點,避免了額外的記憶體分配:
-
在
map[string]
中查詢 key 時,使用了對應的[]byte
,避免做m[string(key)]
的記憶體分配 -
使用
for range
迭代 string 轉換為 []byte 的迭代:for i,v := range []byte(str) {...}
霧:參考原文
本文轉載自https://github.com/wuYin/blog/blob/master/50-shades-of-golang-traps-gotchas-mistakes.md