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 的一部分。