Go Wiki:常見錯誤

目錄

簡介

當新的程式設計師開始使用 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 中進行迭代時,可以使用 goroutines 來平行處理資料。例如,您可以使用封閉函數,寫像下面範例:

for _, val := range values {
    go func() {
        fmt.Println(val)
    }()
}

上述 for 迴圈可能不會執行您的預期功能,因為它們的 val 變數實際上是單一變數,會帶有每個切片元素的值。由於封閉函數都只與那個變數連動,因此當您執行這段程式碼時,很有可能看到每個迭代印出最後一個元素,而不是順序的每個值,因為 goroutines 可能在迴圈後才會開始執行。

撰寫這種封閉函數迴圈的正確方式為

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://go.dev.org.tw/doc/faq#closures_and_goroutines

您可能會發現類似下列情況

for _, val := range values {
    go val.MyMethod()
}

func (v *val) MyMethod() {
    fmt.Println(v)
}

上述範例也會印出值中的最後一個元素,原因與封閉函數相同。要修正問題,請在迴圈內宣告其他變數。

for _, val := range values {
    newVal := val
    go newVal.MyMethod()
}

func (v *val) MyMethod() {
    fmt.Println(v)
}

此內容是 Go Wiki 的一部分。