你有犯過這些 Go 編碼錯誤嗎?
大家好,我是煎魚。
在用 Go 編程時,總會遇到各種奇奇怪怪的錯誤,國內(nèi)外已經(jīng)有許多小伙伴總結(jié)過(參考鏈接見參考),感覺都能湊一桌了。
希望對大家有所幫助。
Go 常見錯誤
1. nil Map
問題
在程序中聲明(定義)了一個 map,然后直接寫入數(shù)據(jù)。如下代碼:
func main() {
var m map[string]string
m["煎魚"] = "進腦子了"
}
輸出結(jié)果:
panic: assignment to entry in nil map
會直接拋出一個 panic。
解決方法
解決方法其實就是要聲明并初始化,Go 里標準寫法是調(diào)用 make 函數(shù)就可以了。如下代碼:
func main() {
m := make(map[string]string)
m["煎魚"] = "下班了"
}
這個問題在初學(xué) Go 時是最容易踩到的錯誤。
2. 空指針的引用
問題
我們在 Go 經(jīng)常會利用結(jié)構(gòu)體去聲明一系列的方法,他看起來向面向?qū)ο笾械?”類“,在業(yè)務(wù)代碼中非常常見。
如下代碼:
type Point struct {
X, Y float64
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
func main() {
var p *Point
fmt.Println(p.Abs())
}
這段程序能夠正常運行嗎?正常計算和輸出?
輸出結(jié)果:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a3143]
goroutine 1 [running]:
main.(*Point).Abs(...)
/Users/eddycjy/awesomeProject/main.go:13
main.main()
/Users/eddycj/awesomeProject/main.go:18 +0x23
直接就恐慌了,由于空指針的引用。
解決方法
如果變量 p 是一個指針,則必須要進行初始化才可以進行調(diào)用。如下代碼:
func main() {
var p *Point = new(Point)
fmt.Println(p.Abs())
}
又或是用值對象的方法來解決:
func main() {
var p Point // has zero value Point{X:0, Y:0}
fmt.Println(p.Abs())
}
3. 使用對循環(huán)迭代器變量的引用
問題
在 Go 中,循環(huán)迭代器變量是一個單一的變量,在每個循環(huán)迭代中取不同的值。這如果使用不當,可能會導(dǎo)致非預(yù)期的行為。
如下代碼:
func main() {
var out []*int
for i := 0; i < 3; i++ {
out = append(out, &i)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
}
輸出結(jié)果是什么。大膽猜想值是 1,2,3,地址都是不一樣的。對嗎?
輸出結(jié)果:
Values: 3 3 3
Addresses: 0x40e020 0x40e020 0x40e020
值都是 3,地址都是同一個指向。
解決方法
其中一種解決方法是將循環(huán)變量復(fù)制到一個新變量中:
for i := 0; i < 3; i++ {
i := i // Copy i into a new variable.
out = append(out, &i)
}
輸出結(jié)果:
Values: 0 1 2
Addresses: 0x40e020 0x40e024 0x40e028
原因是:在每次迭代中,我們將 i 的地址追加到 out 切片中,但由于它是同一個變量,我們實際上追加的是相同的地址,該地址最終包含分配給 i 的最后一個值。
所以只需要拷貝一份,讓兩者脫離關(guān)聯(lián)就可以了。
4. 在循環(huán)迭代器變量上使用 goroutine
問題
在 Go 中進行循環(huán)時,我們經(jīng)常會使用 goroutine 來并發(fā)處理數(shù)據(jù)。最經(jīng)典的就是會結(jié)合閉包來編寫業(yè)務(wù)邏輯。
如下代碼:
values := []int{1, 2, 3, 4, 5}
for _, val := range values {
go func() {
fmt.Println(val)
}()
}
time.Sleep(time.Second)
但在實際的運行中,上述 for 循環(huán)可能無法達到您的預(yù)期,你想的可能是順序輸出切片中的值。
輸出的結(jié)果是:
5
5
4
5
5
你可能會看到每次迭代打印的最后一個元素,甚至你會發(fā)現(xiàn),每次輸出的結(jié)果還不一樣...
如果去掉休眠代碼,會發(fā)現(xiàn) goroutine 可能根本不會開始執(zhí)行,程序就結(jié)束了。
解決方法
這其實就是閉包使用上的一個常見問題,編寫該閉包循環(huán)的正確方法是:
values := []int{1, 2, 3, 4, 5}
for _, val := range values {
go func(val int) {
fmt.Println(val)
}(val)
}
通過將 val 作為參數(shù)添加到閉包中,在每次循環(huán)時,變量 val 都會被存儲在 goroutine 的堆棧中,以確保最終 goroutine 執(zhí)行時值是對的。
當然,這里還有一個隱性問題。大家總會以為是按順序輸出 1, 2, 3, 4, 5。其實不然,因為 goroutine 的執(zhí)行是具有隨機性的,沒法確保順序。
注:經(jīng)常會變形出現(xiàn)在許多 Go 的面試題當中,一旦復(fù)雜起來就容易讓人迷惑。
5. 數(shù)組不會被改變
問題
切片和數(shù)字是我們在 Go 程序中應(yīng)用最廣泛的數(shù)據(jù)類型,但他常常會有一些奇奇怪怪的問題。
如下代碼:
func Foo(a [2]int) {
a[0] = 8
}
func main() {
a := [2]int{1, 2}
Foo(a)
fmt.Println(a)
}
輸出結(jié)是什么。是 [8 2],對嗎?
輸出結(jié)果:
[1 2]
這是為什么,函數(shù)里修改了個寂寞?
解決方法
實際上在 Go 中,所有的函數(shù)傳遞都是值傳遞。也就是將數(shù)組傳遞給函數(shù)時,會復(fù)制該數(shù)組。如果真的是需要傳進函數(shù)內(nèi)修改,可以改用切片。
如下代碼:
func Foo(a []int) {
if len(a) > 0 {
a[0] = 8
}
}
func main() {
a := []int{1, 2}
Foo(a)
fmt.Println(a)
}
輸出結(jié)果:
[8 2]
原因是:切片不會存儲任何的數(shù)據(jù),他的底層 data 會指向一個底層數(shù)組。因此在修改切片的元素時,會修改其底層數(shù)組的相應(yīng)元素,共享同一個底層數(shù)組的其他切片會一并修改。
你以為這就萬事大吉,解決了?并不。當切片擴容時,Go 底層會重新申請新的更大空間,存在與原有切片分離的場景。
因此還是要及時將變更的值返回出來,在主流程上統(tǒng)一處理元數(shù)據(jù)會更好。
總結(jié)
在今天這篇文章中,我們開始了 Go 常見編碼錯誤的第一節(jié),共涉及 5 個案例:
- nil Map。
- 空指針的引用。
- 使用對循環(huán)迭代器變量的引用。
- 在循環(huán)迭代器變量上使用 goroutine。
- 數(shù)組不會被改變。
這些案例非常常見,在單一代碼上看會比較容易發(fā)覺。但一旦混合到應(yīng)用程序中,在繁雜代碼里就比較難看出來。
祝大家吸完后少踩坑,少出 BUG。
參考
golang/go/wiki/CommonMistakes
24 Common Mistakes in Go (gotchas) And How To Avoid Them