不知道有多少 Go 的面試題和洩露,都和 for 迴圈有關。今天我在週末認真一看,發現了 redefining for loop variable semantics 。
著名的硬核大佬 Russ Cox 表示他一直在研究這個問題,並表示十年的經驗表明了當前語意的代價是很大的。
問題
案例一:取地址符
在 Go 語言中,我們寫 for 語句時有時會出現執行和猜想的結果不一致。例如以下第一個案例的程式碼:
var all []*Itemfor _, item := range items {
all = append(all, &item)
}
登入後複製
這段程式碼有問題嗎?變數 all 內的 item 變數,儲存進去的是什麼? 是每次迴圈的 item 值嗎?
實際上在 for 迴圈時,每次存入變數 all 的都是相同的 item,也就是最後一個迴圈的 item 值。
這是 Go 面試裡經常出現的題目,結合 goroutine 更風騷,畢竟還會存在亂序輸出等問題。
如果你想解決這個問題,就需要把程式改寫成如下:
var all []*Itemfor _, 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 中禁止重新定義語言,所以 rsc 不能直接這麼幹。
因此將會由使用者自己決定控制這個 「破壞」,方式將會是根據每個包的 go.mod 檔案中的 go 行更改語意。
如果我們是在 Go1.30 對本文討論的 for 迴圈改為迭代,那麼在 go.mod 檔案中的 go 版本宣告是將是一個關鍵。
如下圖示:
php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:
Go 1.30 或更高版本將會每次迭代變數,而早期 Go 版本的將每次迴圈變數。
如此一來上述提到的 for 迴圈問題都會在一定範圍內被解決。
總結
for 迴圈時的變數問題,一直是各大 Go 考官愛考的題目,另外也確實在實際程式設計 Go 程式碼時會遇到這類坑。
雖然 rsc 希望在 go.mod 檔案上開創先河,利用 go 版本的宣告,去修改語意(不允許新增和刪除)。這無疑是給 Go1 相容性保障開了一個後門。
如果實施,本次變更會導致 Go 的前後版本語意有所不同。還不如變成一個 go.mod 檔案的一個語意開關。
這顯然是一個很折騰的思考題。
以上就是Go語言中的for迴圈有多坑?的詳細內容,更多請關注TW511.COM其它相關文章!