大多數(shù)Go程序員都走過的坑盤點解析
循環(huán)變量
說起每個程序員必犯的錯誤,那還得是"循環(huán)變量"這個錯誤了,就連 Go 的開發(fā)者都犯過這個錯誤,這個錯誤在 Go 的 FAQ 中也有提到
What happens with closures running as goroutines?[1]:
func main() { var wg sync.WaitGroup values := []string{"a", "b", "c"} for _, v := range values { wg.Add(1) go func() { fmt.Println(v) wg.Done() }() } wg.Wait() }
你可能期望能輸出a
、b
、c
這三個字符(可能順序不同),但是實際可能輸出的是c
、c
、c
。這是因為循環(huán)變量的作用域是整個循環(huán),而不是單次迭代,所以在循環(huán)體中使用的變量是同一個變量,而不是每次迭代都是一個新的變量。
這個錯誤有時候隱藏很深,即使沒有 goroutine,也有可能,比如下面的代碼,并沒有使用額外的 goroutine 和閉包,也是有問題的:
package main import ( "fmt" ) type Char struct { Char *string } func main() { var chars []Char values := []string{"a", "b", "c"} for _, v := range values { chars = append(chars, Char{Char: &v}) } for _, v := range chars { fmt.Println(*v.Char) } }
輸出也大概率是c
、c
、c
,因為給每個Char
的字段賦值的是 v 的指針,v 在整個循環(huán)中都是一個變量,所以最后的結果都是c
。
Go 團隊很早也意識到這個問題了,但是考慮到兼容的問題,大家的容忍程度,那就這樣了。每個 Go 程序員都在這里摔一跤,也就長記性了,所以一直沒有改變這個設計。我在這里摔了好多跤,以至于我寫 for 循環(huán)的時候都戰(zhàn)戰(zhàn)兢兢的,和 Russ Cox 統(tǒng)計的網上的處理一樣,不管有無必要,很多時候我都是先把循環(huán)變量賦值給一個局部變量,然后再使用,比如下面的代碼:
for _, v := range values { v := v wg.Add(1) go func() { fmt.Println(v) wg.Done() }() }
變量只在循環(huán)體中使用
今年 5 月份的時候,Russ Cox 忍不住了,提了一個提案#60078[2],提案的內容是在 for 循環(huán)中,如果變量只在循環(huán)體中使用,那么就會在每次迭代中創(chuàng)建一個新的變量,而不是使用同一個變量。這個提案引起了很多人的關注,很多人都在討論這個提案,這個提案被接收了,具體提案內容在文檔中Proposal: Less Error-Prone Loop Variable Scoping[3]。
如果你使用 Go 1.21, 你可以開始這個功能,使用GOEXPERIMENT=loopvar go run main.go
運行上面的程序,會輸出c
、b
、a
這樣的輸出,不再是c
、c
、c
了。這個特性在 Go 1.22 中會默認開啟,不需要設置GOEXPERIMENT
了。還有一兩個月才能正式發(fā)布 go 1.22,
大家可以使用 gotip 測試:
$ gotip run main.go a b c
不只是for-range
,下面的3-clause
也是同樣的問題:
func main() { var ids []*int for i := 0; i < 3; i++ { i = 10 } for _, id := range ids { fmt.Println(*id) } }
Go 1.22 中也會修復這個問題。
對比C#語言
C#語言就只修改了for-range
語句,3-clause
語句就沒有修改, Go 兩種都做了修改。
但是, 問題就來了哈,像下面的代碼,Go 1.22 和以前的代碼會一樣么?
func main() { var ids []*int for i := 0; i < 3; i++ { i = 10 ids = append(ids, &i) } for _, id := range ids { fmt.Println(*id) } }
如果用 Go 1.21,它會輸出11
。如果用 Go 1.22,它會輸出10
。原因還是在于這個提案實現(xiàn)后,每次迭代的時候,都會創(chuàng)建一個新的變量,所以ids
中的元素都是指向不同的變量,而不是同一個變量。
看起來打破了向下兼容的承諾,你如果先前就想利用這個 corner case 的話,Go1.22 已經不兼容了。
更進一步,你會發(fā)現(xiàn)再執(zhí)行3-clause
的第三條 clause 的時候,變量已經被重新創(chuàng)建,比如下面的代碼:
func main() { for i, p := 0, (*int)(nil); i < 3; println("3rd-clause:", &i, p) { p = &i fmt.Println("loop body:", &i, p) i++ } }
輸出:
$gotip run main.go
loop body: 0x14000120018 0x14000120018
3rd-clause: 0x14000120030 0x14000120018 // &i已經變?yōu)?x14000120030
loop body: 0x14000120030 0x14000120030
3rd-clause: 0x14000120038 0x14000120030 // &i已經變?yōu)?x14000120038
loop body: 0x14000120038 0x14000120038
3rd-clause: 0x14000120040 0x14000120038 // &i已經變?yōu)?x14000120040
參考資料
https://github.com/golang/go/issues/60078
https://go.dev/doc/faq#closures_and_goroutines
以上就是大多數(shù)Go程序員都走過的坑盤點解析的詳細內容,更多關于Go程序員都走過的坑的資料請關注腳本之家其它相關文章!
相關文章
詳解golang 定時任務time.Sleep和time.Tick實現(xiàn)結果比較
本文主要介紹了golang 定時任務time.Sleep和time.Tick實現(xiàn)結果比較,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02Go語言實現(xiàn)Fibonacci數(shù)列的方法
這篇文章主要介紹了Go語言實現(xiàn)Fibonacci數(shù)列的方法,實例分析了使用遞歸和不使用遞歸兩種技巧,并對算法的效率進行了對比,需要的朋友可以參考下2015-02-02