Golang 新手要注意的陷阱和常見錯誤(一)
Go 是一門簡單有趣的語言,但與其他語言類似,它會有一些技巧。。。這些技巧的絕大部分并不是 Go 的缺陷造成的。如果你以前使用的是其他語言,那么這其中的有些錯誤就是很自然的陷阱。其它的是由錯誤的假設(shè)和缺少細節(jié)造成的。
如果你花時間學(xué)習(xí)這門語言,閱讀官方說明、wiki、郵件列表討論、大量的優(yōu)秀博文和 Rob Pike 的展示,以及源代碼,這些技巧中的絕大多數(shù)都是顯而易見的。盡管不是每個人都是以這種方式開始學(xué)習(xí)的,但也沒關(guān)系。如果你是 Go 語言新人,那么這里的信息將會節(jié)約你大量的調(diào)試代碼的時間。
一. 初級篇
1. 開大括號不能放在單獨的一行
在大多數(shù)其他使用大括號的語言中,你需要選擇放置它們的位置。Go 的方式不同。你可以為此感謝下自動分號的注入(沒有預(yù)讀)。是的, Go 中也是有分號的:-)
失敗的例子:
package mainimport "fmt"func main(){ //error, can't have the opening brace on a separate linefmt.Println("hello there!")}
編譯錯誤:
/tmp/sandbox826898458/main.go:6:?syntax?error:?unexpected?semicolon?or?newline?before?{有效的例子:
package mainimport "fmt"func main() {fmt.Println("works!")}
2. 未使用的變量
如果你有未使用的變量,代碼將編譯失敗。當(dāng)然也有例外。在函數(shù)內(nèi)一定要使用聲明的變量,但未使用的全局變量是沒問題的。
如果你給未使用的變量分配了一個新的值,代碼還是會編譯失敗。你需要在某個地方使用這個變量,才能讓編譯器愉快的編譯。
Fails:
package mainvar gvar int //not an errorfunc main() {var one int //error, unused variabletwo := 2 //error, unused variablevar three int //error, even though it's assigned 3 on the next linethree = 3}
Compile Errors:
/tmp/sandbox473116179/main.go:6: one declared and not used/tmp/sandbox473116179/main.go:7: two declared and not used/tmp/sandbox473116179/main.go:8:?three?declared?and?not?used
Works:
package mainimport "fmt"func main() {var one int_ = onetwo := 2fmt.Println(two)var three intthree = 3one = threevar four intfour = four}
另一個選擇是注釋掉或者移除未使用的變量?
3. 未使用的 import
如果你引入一個包,而沒有使用其中的任何函數(shù)、接口、結(jié)構(gòu)體或者變量的話,代碼將會編譯失敗。
你可以使用 goimports 來增加引入或者移除未使用的引用:
go get golang.org/x/tools/cmd/goimports如果你真的需要引入的包,你可以添加一個下劃線標(biāo)記符 _ ,來作為這個包的名字,從而避免編譯失敗。下滑線標(biāo)記符用于引入,但不使用。
package mainimport ("fmt""log""time")func main() {}
Compile Errors:
/tmp/sandbox627475386/main.go:4: imported and not used: "fmt"/tmp/sandbox627475386/main.go:5: imported and not used: "log"/tmp/sandbox627475386/main.go:6:?imported?and?not?used:?"time"
Works:
package mainimport (_ "fmt""log""time")var _ = log.Printlnfunc main() {_ = time.Now}
另一個選擇是移除或者注釋掉未使用的 import
4. 簡式的變量聲明僅可以在函數(shù)內(nèi)部使用
Fails:
package mainmyvar := 1 //errorfunc main() {}
Compile Error:
/tmp/sandbox265716165/main.go:3:?non-declaration?statement?outside?function?bodyWorks:
package mainvar myvar = 1func main() {}
5. 使用簡式聲明重復(fù)聲明變量
你不能在一個單獨的聲明中重復(fù)聲明一個變量,但在多變量聲明中這是允許的,其中至少要有一個新的聲明變量。
重復(fù)變量需要在相同的代碼塊內(nèi),否則你將得到一個隱藏變量。
Fails:
package mainfunc main() {one := 0one := 1 //error}
Compile Error:
/tmp/sandbox706333626/main.go:5:?no?new?variables?on?left?side?of?:=Works:
package mainfunc main() {one := 0one, two := 1,2one,two = two,one}
6. 偶然的變量隱藏 Accidental Variable Shadowing
短式變量聲明的語法如此的方便(尤其對于那些使用過動態(tài)語言的開發(fā)者而言),很容易讓人把它當(dāng)成一個正常的分配操作。如果你在一個新的代碼塊中犯了這個錯誤,將不會出現(xiàn)編譯錯誤,但你的應(yīng)用將不會做你所期望的事情。
package mainimport "fmt"func main() {x := 1fmt.Println(x) //prints 1{fmt.Println(x) //prints 1x := 2fmt.Println(x) //prints 2}fmt.Println(x) //prints 1 (bad if you need 2)}
即使對于經(jīng)驗豐富的Go開發(fā)者而言,這也是一個非常常見的陷阱。這個坑很容易挖,但又很難發(fā)現(xiàn)。
你可以使用 vet 命令來發(fā)現(xiàn)一些這樣的問題。默認情況下, vet不會執(zhí)行這樣的檢查,你需要設(shè)置 -shadow參數(shù):go tool vet -shadow your_file.go
7. 不使用顯式類型,無法使用“nil”來初始化變量
nil 標(biāo)志符用于表示 interface 、函數(shù)、 maps 、 slices 和 channels 的“零值”。如果你不指定變量的類型,編譯器將無法編譯你的代碼,因為它猜不出具體的類型。
Fails:
package mainfunc main() {var x = nil //error_ = x}
Compile Error:
/tmp/sandbox188239583/main.go:4: use of untyped nilWorks:
package mainfunc main() {var x interface{} = nil_ = x}
8. 使用“nil” Slices and Maps
在一個 nil 的 slice 中添加元素是沒問題的,但對一個 map 做同樣的事將會生成一個運行時的 panic 。
Works:
package mainfunc main() {var s []ints = append(s,1)}
Fails:
package mainfunc main() {var m map[string]intm["one"] = 1 //error}
9. map的容量
你可以在 map 創(chuàng)建時指定它的容量,但你無法在 map 上使用 cap() 函數(shù)。
Fails:
package mainfunc main() {m := make(map[string]int,99)cap(m) //error}
Compile Error:
/tmp/sandbox326543983/main.go:5:?invalid?argument?m?(type?map[string]int)?for?cap
10. 字符串不會為nil
這對于經(jīng)常使用 nil 分配字符串變量的開發(fā)者而言是個需要注意的地方。
Fails:
package mainfunc main() {var x string = nil //errorif x == nil { //errorx = "default"}}
Compile Errors:
/tmp/sandbox630560459/main.go:4:?cannot?use?nil?as?type?string?in?assignment?/tmp/sandbox630560459/main.go:6:?invalid?operation:?x?==?nil?(mismatched?types?string?and?nil)Works:
package mainfunc main() {var x string //defaults to "" (zero value)if x == "" {x = "default"}}
11. array 函數(shù)的參數(shù)
如果你是一個 C 或則 C++ 開發(fā)者,那么數(shù)組對你而言就是指針。當(dāng)你向函數(shù)中傳遞數(shù)組時,函數(shù)會參照相同的內(nèi)存區(qū)域,這樣它們就可以修改原始的數(shù)據(jù)。
Go 中的數(shù)組是數(shù)值,因此當(dāng)你向函數(shù)中傳遞數(shù)組時,函數(shù)會得到原始數(shù)組數(shù)據(jù)的一份復(fù)制。如果你打算更新數(shù)組的數(shù)據(jù),這將會是個問題。
package mainimport "fmt"func main() {x := [3]int{1,2,3}func(arr [3]int) {arr[0] = 7fmt.Println(arr) //prints [7 2 3]}(x)fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])}
如果你需要更新原始數(shù)組的數(shù)據(jù),你可以使用數(shù)組指針類型。
package mainimport "fmt"func main() {x := [3]int{1,2,3}func(arr *[3]int) {(*arr)[0] = 7fmt.Println(arr) //prints &[7 2 3]}(&x)fmt.Println(x) //prints [7 2 3]}
另一個選擇是使用 slice 。即使你的函數(shù)得到了 slice 變量的一份拷貝,它依舊會參照原始的數(shù)據(jù)。
package mainimport "fmt"func main() {x := []int{1,2,3}func(arr []int) {arr[0] = 7fmt.Println(arr) //prints [7 2 3]}(x)fmt.Println(x) //prints [7 2 3]}
12. 在 slice 和 array 使用“range”語句時的出現(xiàn)的不希望得到的值
如果你在其他的語言中使用 for-in 或者 foreach 語句時會發(fā)生這種情況。Go 中的 range 語法不太一樣。它會得到兩個值:第一個值是元素的索引,而另一個值是元素的數(shù)據(jù)。
Bad:
package mainimport "fmt"func main() {x := []string{"a","b","c"}for v := range x {fmt.Println(v) //prints 0, 1, 2}}
Good:
package mainimport "fmt"func main() {x := []string{"a","b","c"}for _, v := range x {fmt.Println(v) //prints a, b, c}}
13. slices 和 arrays 是一維的
看起來 Go 好像支持多維的 Array 和 Slice ,但不是這樣的。盡管可以創(chuàng)建數(shù)組的數(shù)組或者切片的切片。對于依賴于動態(tài)多維數(shù)組的數(shù)值計算應(yīng)用而言, Go 在性能和復(fù)雜度上還相距甚遠。
你可以使用純一維數(shù)組、“獨立”切片的切片,“共享數(shù)據(jù)”切片的切片來構(gòu)建動態(tài)的多維數(shù)組。
如果你使用純一維的數(shù)組,你需要處理索引、邊界檢查、當(dāng)數(shù)組需要變大時的內(nèi)存重新分配。
使用“獨立” slice 來創(chuàng)建一個動態(tài)的多維數(shù)組需要兩步。首先,你需要創(chuàng)建一個外部的 slice 。然后,你需要分配每個內(nèi)部的 slice 。內(nèi)部的 slice 相互之間獨立。你可以增加減少它們,而不會影響其他內(nèi)部的 slice 。
package mainfunc main() {x := 2y := 4table := make([][]int,x)for i:= range table {table[i] = make([]int,y)}}
使用“共享數(shù)據(jù)” slice 的 slice 來創(chuàng)建一個動態(tài)的多維數(shù)組需要三步。首先,你需要創(chuàng)建一個用于存放原始數(shù)據(jù)的數(shù)據(jù)“容器”。然后,你再創(chuàng)建外部的 slice 。最后,通過重新切片原始數(shù)據(jù) slice 來初始化各個內(nèi)部的 slice
package mainimport "fmt"func main() {h, w := 2, 4raw := make([]int,h*w)for i := range raw {raw[i] = i}fmt.Println(raw,&raw[4])//prints: [0 1 2 3 4 5 6 7]table := make([][]int,h)for i:= range table {table[i] = raw[i*w:i*w + w]}fmt.Println(table,&table[1][0])//prints: [[0 1 2 3] [4 5 6 7]]}
關(guān)于多維 array 和 slice 已經(jīng)有了專門申請,但現(xiàn)在看起來這是個低優(yōu)先級的特性。
14. 訪問不存在的 map keys
這對于那些希望得到 nil 標(biāo)示符的開發(fā)者而言是個技巧(和其他語言中做的一樣)。如果對應(yīng)的數(shù)據(jù)類型的“零值”是 nil ,那返回的值將會是 nil ,但對于其他的數(shù)據(jù)類型是不一樣的。檢測對應(yīng)的“零值”可以用于確定 map 中的記錄是否存在,但這并不總是可信(比如,如果在二值的 map 中“零值”是 false ,這時你要怎么做)。檢測給定 map 中的記錄是否存在的最可信的方法是,通過 map 的訪問操作,檢查第二個返回的值。
Bad:
package mainimport "fmt"func main() {x := map[string]string{"one":"a","two":"","three":"c"}if v := x["two"]; v == "" { //incorrectfmt.Println("no entry")}}
Good:
package mainimport "fmt"func main() {x := map[string]string{"one":"a","two":"","three":"c"}if _,ok := x["two"]; !ok {fmt.Println("no entry")}}
15. Strings 無法修改
嘗試使用索引操作來更新字符串變量中的單個字符將會失敗。string 是只讀的 byte slice (和一些額外的屬性)。如果你確實需要更新一個字符串,那么使用 byte slice ,并在需要時把它轉(zhuǎn)換為 string 類型。
Fails:
package mainimport "fmt"func main() {x := "text"x[0] = 'T'fmt.Println(x)}
Compile Error:
/tmp/sandbox305565531/main.go:7:?cannot?assign?to?x[0]Works:
package mainimport "fmt"func main() {x := "text"xbytes := []byte(x)xbytes[0] = 'T'fmt.Println(string(xbytes)) //prints Text}
需要注意的是:這并不是在文字string中更新字符的正確方式,因為給定的字符可能會存儲在多個byte中。如果你確實需要更新一個文字string,先把它轉(zhuǎn)換為一個rune slice。
即使使用 rune slice ,單個字符也可能會占據(jù)多個 rune ,比如當(dāng)你的字符有特定的重音符號時就是這種情況。這種復(fù)雜又模糊的“字符”本質(zhì)是 Go 字符串使用 byte 序列表示的原因。
