Go for 循環有時候真的很坑

大家好,我是煎魚。

不知道有多少 Go 的面試題和泄露,都和 for 循環有關。今天我在週末認真一看,發現了 redefining for loop variable semantics[1] ,看來大家踩到的坑都是一樣的。

著名的硬核大佬 Russ Cox 表示他一直在研究這個問題,表示十年的經驗表明了當前語義的代價是很大的,得動一動,看看能不能打破兼容性原則。

想了下之前 Go modules 的事情,我真怕他一口氣就把這塔給推了...

問題

案例一

在 Go 語言中,我們寫 for 語句時有時會出現運行和猜想的結果不一致。例如以下第一個案例的代碼:

var all []*Item
for _, item := range items {
 all = append(all, &item)
}

這段代碼有問題嗎?變量 all 內的 item 變量,存儲進去的是什麼?是每次循環的 item 值,每次都不一樣,對嗎?

實際上在 for 循環時,每次存入變量 all 的都是相同的 item,也就是最後一個循環的 item 值。這是 Go 面試裏經常出現的題目,結合 goroutine 更風騷,畢竟還會存在亂序執行等問題。

如果你想解決這個問題,就需要把程序改寫成如下:

var all []*Item
for _, item := range items {
 item := item
 all = append(all, &item)
}

要重新聲明一個局部變量 item 變量,把 for 循環的 item 變量給存儲下來,再追加進去。

案例二

接下來是第二個案例的代碼:

var prints []func()
for _, v := range []int{1, 2, 3} {
 prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
 print()
}

這段程序的輸出結果是什麼?沒有 & 取地址符,是輸出 1,2,3 嗎?

結果程序一運行,輸出結果是 3,3,3。這又是爲什麼?

問題的重點之一:關注到閉包函數,實際上所有閉包都打印的是相同的 v,也就是輸出 3,原因是在 for 循環結束後,最後 v 的值被設置爲了 3,僅此而已。

如果想要達到預期的效果,依然是使用萬能的再賦值。改寫後的代碼如下:

for _, v := range []int{1, 2, 3} {
  v := v
  prints = append(prints, func() { fmt.Println(v) })
 }

增加 v := v 語句,程序輸出結果爲 1,2,3。仔細翻翻你寫過的 Go 工程,是不是都很熟悉?就這改造方法,贏了。

尤其是配合上 Goroutine 的寫法,很多同學會更容易在此翻車。

解決方案

修復思路

實際上 Go 核心團隊在內部和社區已經討論過許久,希望重新定義 for 循環的語義。要達到的目的是:使循環變量每次迭代而不是每次循環

解決的辦法是:在每個迭代變量 x 的每個循環體開頭,加一個隱式的再賦值,也就是 x := x,就能夠解決上述程序中所隱含的坑。

和我們現在做的一樣,只不過我們是自己手動加的,Go 團隊做的是希望在編譯器內隱式處理。

讓用戶自己決定

比較尷尬的是 Go 團隊在 Proposal: Go 2 transition[2] 中明確禁止重新定義語言的語義,所以 rsc 不能直接這麼幹。

因此 rsc 打算開個新坑,希望將會由用戶自己決定控制這個 “破壞”,方式將會是根據每個 modules 的 go.mod 文件中的 go 行(版本聲明)來決定語義

例如,如果是在 Go1.30 對本文討論的 for 循環將循環變量改爲迭代,那麼在 go.mod 文件中的 go 版本聲明是將是一個關鍵的開關。

如下圖示:

像上圖的配置,Go 1.30 或更高版本將會每次迭代變量,而早期 Go 版本的將每次循環變量,也就是 go.mod 的 Go 版本控制了新特性的語義,不同 modules 都可能會因此不一樣。

如此一來上述提到的 for 循環問題都會在一定範圍內被解決。

總結

for 循環時的變量問題,一直是各大 Go 考官愛考的題目,也確實在實際編程 Go 代碼時會遇到這類坑。

雖然 rsc 希望在 go.mod 文件上開創先河,利用 go 版本的聲明,去修改語義(不允許添加和刪除)。這無疑是給 Go1 兼容性保障開了一個後門。

如果實施,本次變更會導致 Go 的前後版本語義有所不同。還不如變成一個 go.mod 文件的一個語義開關,一變全變,否則這種變一些不變一些的,會給問題排查和理解上帶來不少的成本。

這顯然是一個很折騰人的思考題。

參考資料

[1]

redefining for loop variable semantics: https://github.com/golang/go/discussions/56010

[2]

Proposal: Go 2 transition: https://github.com/golang/proposal/blob/master/design/28221-go2-transitions.md#language-changes

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Pcp2FjcXk7Q7sV0Z-bEUNw