Go Wiki:常見錯誤
目錄
引言
當新 Go 程式設計師開始使用 Go,或者老 Go 程式設計師開始使用新概念時,會犯一些常見的錯誤。下面是一些在郵件列表和 IRC 中經常出現的、不詳盡的常見錯誤列表。
使用迴圈迭代器變數的引用
注意:以下部分適用於 Go < 1.22。Go 版本 >= 1.22 使用作用域限定到迭代的變數,有關詳細資訊,請參閱 修復 Go 1.22 中的 for 迴圈。
在 Go 中,迴圈迭代器變數是一個在每次迴圈迭代中取不同值的單個變數。這非常高效,但如果使用不當可能會導致意外行為。例如,請看以下程式
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])
}
它將輸出意外的結果
Values: 3 3 3
Addresses: 0x40e020 0x40e020 0x40e020
解釋:在每次迭代中,我們都將 i 的地址追加到 out 切片中,但由於它是一個相同的變數,我們追加的是相同的地址,該地址最終包含的是賦給 i 的最後一個值。解決方案之一是將迴圈變數複製到一個新變數中
for i := 0; i < 3; i++ {
+ i := i // Copy i into a new variable.
out = append(out, &i)
}
程式的輸出現在符合預期
Values: 0 1 2
Addresses: 0x40e024 0x40e028 0x40e032
解釋:i := i 這行程式碼將迴圈變數 i 複製到一個新變數中,該變數的作用域限定在 for 迴圈體塊內,也稱為 i。新變數的地址被追加到陣列中,使其生命週期超出 for 迴圈體塊。每次迴圈迭代都會建立一個新變數。
雖然這個例子可能看起來有點明顯,但在其他一些情況下,相同的意外行為可能更隱蔽。例如,迴圈變數可以是陣列,而引用可以是切片
func main() {
var out [][]int
for _, i := range [][1]int{{1}, {2}, {3}} {
out = append(out, i[:])
}
fmt.Println("Values:", out)
}
輸出
Values: [[3] [3] [3]]
當迴圈變數在 Goroutine 中使用時(請參閱下一節),也會出現同樣的問題。
在迴圈迭代器變數上使用 goroutine
注意:以下部分適用於 Go < 1.22。Go 版本 >= 1.22 使用作用域限定到迭代的變數,有關詳細資訊,請參閱 修復 Go 1.22 中的 for 迴圈。
在 Go 中進行迭代時,可能會嘗試使用 goroutine 並行處理資料。例如,您可能會這樣編寫,使用閉包
for _, val := range values {
go func() {
fmt.Println(val)
}()
}
上面的 for 迴圈可能不會如您所料地執行,因為它們的 val 變數實際上是一個在每次迭代中獲取切片元素值的單個變數。由於所有的閉包都只繫結到這一個變數,當您執行此程式碼時,很可能會看到每次迭代都打印出最後一個元素,而不是按順序列印每個值,因為 goroutine 很可能在迴圈結束後才會開始執行。
編寫該閉包迴圈的正確方法是
for _, val := range values {
go func(val interface{}) {
fmt.Println(val)
}(val)
}
透過將 val 作為閉包的引數,val 在每次迭代時都會被求值並放在 goroutine 的堆疊上,因此當 goroutine 最終執行時,每個切片元素都可用。
同樣需要注意的是,在迴圈體內部宣告的變數在迭代之間不會共享,因此可以在閉包中獨立使用。以下程式碼使用了一個通用的索引變數 i 來建立獨立的 val,從而實現了預期的行為
for i := range valslice {
val := valslice[i]
go func() {
fmt.Println(val)
}()
}
請注意,如果不在 goroutine 中執行此閉包,程式碼會按預期執行。以下示例將打印出 1 到 10 之間的整數。
for i := 1; i <= 10; i++ {
func() {
fmt.Println(i)
}()
}
即使所有的閉包仍然閉合在同一個變數(在本例中是 i),它們也會在變數改變之前執行,從而產生期望的行為。 https://golang.com.tw/doc/faq#closures_and_goroutines
您可能會遇到另一種類似的情況,如下所示
for _, val := range values {
go val.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
上面的示例也會列印 values 的最後一個元素,原因與閉包相同。要解決這個問題,請在迴圈內部宣告另一個變數。
for _, val := range values {
newVal := val
go newVal.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
此內容是 Go Wiki 的一部分。