Go 陷阱之 for 迴圈迭代變數

JaguarJack發表於2019-04-06

原文轉載 Go 陷阱之 for 迴圈迭代變數

捕獲迭代變數

這是在學習 Go 程式設計 中遇到的一個比較重要的一個警告。這是個 Go 語言的詞法作用域規則的陷阱。看完之後感覺是真的一個比較讓人疑惑困惑的地方。所以特地記錄一下。由標題就可以知道了,迭代變數,肯定是在 for 中遇到的問題。來看一個簡單的例子說明一下這個問題所在。

看一段簡單的程式碼, 首先是錯誤的示例:

var slice []func()

func main() {
    sli := []int{1, 2, 3, 4, 5}
    for _, v := range sli {
        fmt.Println(&v)
        slice = append(slice, func(){
            fmt.Println(v * v) // 直接列印結果
        })
    }

    for _, val  := range slice {
        val()
    }
}
// 輸出 25 25 25 25 25

你可能會很奇怪為什麼會出現這種情況, 結果不應該是 1, 4, 9, 16, 25 嗎?其實原因是迴圈變數的作用域的規則限制。在上面的程式中,v 在 for 迴圈引進的一個塊作用域內進行宣告。在迴圈裡建立的所有函式變數共享相同的變數,就是一個可訪問的儲存位置,而不是固定的值。(你會驚奇的發現 &v 的記憶體地址是一樣的)

模擬一下實際的情況,假設 v 變數的地址在 0x12345678 上, for 迴圈在迭代過程中,所有變數值都是在這地址上迭代的。當最後呼叫匿名函式的時候,取值也是在這塊地址上。所以最後輸出的結果都是迭代的最後一個值。至少在 Go 語言中是不用質疑的。這裡也是一個陷阱,如果你不清楚的話,肯定會遇到坑。那個該如何修改呢?

var slice []func()

func main() {
    sli := []int{1, 2, 3, 4, 5}
    for _, v := range sli {
        temp := v // 其實很簡單 引入一個臨時區域性變數就可以了,這樣就可以將每次的值儲存到該變數地址上
        fmt.Println(&temp) // 這裡記憶體地址是不同的
        slice = append(slice, func(){
            fmt.Println(temp *  temp) // 直接列印結果
        })
    }

    for _, val  := range slice {
        val()
    }
}
// 輸出 1, 4, 9, 16, 25 預期結果

只需要引入一個區域性變數便可以解決了,這是必須的。否則你的程式將不會有可預期的結果。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章