本文由golang教學專欄跟大家介紹關於Go for 迴圈的面試問題,不知道大家對for迴圈了解多少,有沒有覺得很坑?以下就給大家詳細聊聊for相關問題,希望對需要的朋友有幫助!
不知道有多少 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 版本宣告就是一個關鍵。
如下圖示:
Go 1.30 或更高版本將會每次迭代變量,而早期 Go 版本的將每次循環變數。
如此一來上述提到的 for 迴圈問題都會在一定範圍內解決。
總結
for 迴圈時的變數問題,一直是各大Go 考官愛考的題目,另外也確實在實際程式設計Go 程式碼時會遇到這類坑。
雖然 rsc 希望在 go.mod 檔案上開創先河,利用 go 版本的聲明,去修改語意(不允許新增和刪除)。這無疑是給 Go1 相容性保障開了一個後門。
如果實施,本次變更會導致 Go 的前後版本語意有所不同。還不如變成一個 go.mod 檔案的一個語義開關。
這顯然是一個很折騰的思考題。
以上是Go語言中的for迴圈有多坑?的詳細內容。更多資訊請關注PHP中文網其他相關文章!