Go for 循環(huán)有時候真的很坑。。。
大家好,我是煎魚。
不知道有多少 Go 的面試題和泄露,都和 for 循環(huán)有關(guān)。今天我在周末認(rèn)真一看,發(fā)現(xiàn)了 redefining for loop variable semantics[1] ,看來大家踩到的坑都是一樣的。
著名的硬核大佬 Russ Cox 表示他一直在研究這個問題,表示十年的經(jīng)驗表明了當(dāng)前語義的代價是很大的,得動一動,看看能不能打破兼容性原則。
想了下之前 Go modules 的事情,我真怕他一口氣就把這塔給推了...
問題
案例一
在 Go 語言中,我們寫 for 語句時有時會出現(xiàn)運行和猜想的結(jié)果不一致。例如以下第一個案例的代碼:
這段代碼有問題嗎?變量 all 內(nèi)的 item 變量,存儲進去的是什么?是每次循環(huán)的 item 值,每次都不一樣,對嗎?
實際上在 for 循環(huán)時,每次存入變量 all 的都是相同的 item,也就是最后一個循環(huán)的 item 值。這是 Go 面試?yán)锝?jīng)常出現(xiàn)的題目,結(jié)合 goroutine 更風(fēng)騷,畢竟還會存在亂序執(zhí)行等問題。
如果你想解決這個問題,就需要把程序改寫成如下:
要重新聲明一個局部變量 item 變量,把 for 循環(huán)的 item 變量給存儲下來,再追加進去。
案例二
接下來是第二個案例的代碼:
這段程序的輸出結(jié)果是什么?沒有 & 取地址符,是輸出 1,2,3 嗎?
結(jié)果程序一運行,輸出結(jié)果是 3,3,3。這又是為什么?
問題的重點之一:關(guān)注到閉包函數(shù),實際上所有閉包都打印的是相同的 v,也就是輸出 3,原因是在 for 循環(huán)結(jié)束后,最后 v 的值被設(shè)置為了 3,僅此而已。
如果想要達到預(yù)期的效果,依然是使用萬能的再賦值。改寫后的代碼如下:
增加 v := v 語句,程序輸出結(jié)果為 1,2,3。仔細(xì)翻翻你寫過的 Go 工程,是不是都很熟悉?就這改造方法,贏了。
尤其是配合上 Goroutine 的寫法,很多同學(xué)會更容易在此翻車。
解決方案
修復(fù)思路
實際上 Go 核心團隊在內(nèi)部和社區(qū)已經(jīng)討論過許久,希望重新定義 for 循環(huán)的語義。要達到的目的是:使循環(huán)變量每次迭代而不是每次循環(huán)。
解決的辦法是:在每個迭代變量 x 的每個循環(huán)體開頭,加一個隱式的再賦值,也就是 x := x,就能夠解決上述程序中所隱含的坑。
和我們現(xiàn)在做的一樣,只不過我們是自己手動加的,Go 團隊做的是希望在編譯器內(nèi)隱式處理。
讓用戶自己決定
比較尷尬的是 Go 團隊在 Proposal: Go 2 transition[2] 中明確禁止重新定義語言的語義,所以 rsc 不能直接這么干。
因此 rsc 打算開個新坑,希望將會由用戶自己決定控制這個 “破壞”,方式將會是根據(jù)每個 modules 的 go.mod 文件中的 go 行(版本聲明)來決定語義。
例如,如果是在 Go1.30 對本文討論的 for 循環(huán)將循環(huán)變量改為迭代,那么在 go.mod 文件中的 go 版本聲明是將是一個關(guān)鍵的開關(guān)。
如下圖示:
像上圖的配置,Go 1.30 或更高版本將會每次迭代變量,而早期 Go 版本的將每次循環(huán)變量,也就是 go.mod 的 Go 版本控制了新特性的語義,不同 modules 都可能會因此不一樣。
如此一來上述提到的 for 循環(huán)問題都會在一定范圍內(nèi)被解決。
總結(jié)
for 循環(huán)時的變量問題,一直是各大 Go 考官愛考的題目,也確實在實際編程 Go 代碼時會遇到這類坑。
雖然 rsc 希望在 go.mod 文件上開創(chuàng)先河,利用 go 版本的聲明,去修改語義(不允許添加和刪除)。這無疑是給 Go1 兼容性保障開了一個后門。
如果實施,本次變更會導(dǎo)致 Go 的前后版本語義有所不同。還不如變成一個 go.mod 文件的一個語義開關(guān),一變?nèi)儯駝t這種變一些不變一些的,會給問題排查和理解上帶來不少的成本。
這顯然是一個很折騰人的思考題。