最近準備寫一些關於golang的技術博文,本文是以前在GitHub上看到的golang技術譯文,感受頗有幫助,先給各位讀者分享一下。php
前言
Go 是一門簡單有趣的編程語言,與其餘語言同樣,在使用時難免會遇到不少坑,不過它們大多不是 Go 自己的設計缺陷。若是你剛從其餘語言轉到 Go,那這篇文章裏的坑多半會踩到。html
若是花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減小大量調試代碼的時間。git
初級篇:1-35
1. 左大括號 {
通常不能單獨放一行
在其餘大多數語言中,{
的位置你自行決定。Go 比較特別,遵照分號注入規則(automatic semicolon injection):編譯器會在每行代碼尾部特定分隔符後加 ;
來分隔多條語句,好比會在 )
後加分號:github
// 錯誤示例 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") } }
考:Golang中自動加分號的特殊分隔符golang
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 一個包,但包中的變量、函數、接口和結構體一個都沒有用到的話,將編譯失敗。app
可使用 _
下劃線符號做爲別名來忽略導入的包,從而避免編譯錯誤,這隻會執行 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() { } // 正確示例 var myvar = 1 func main() { }
5. 使用簡短聲明來重複聲明變量
不能用簡短聲明方式來單獨爲一個變量重複聲明, :=
左側至少有一個新變量,才容許多變量的重複聲明:ide
// 錯誤示例 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) {...}
霧:參考原文
- 在
-