在「Fixing for loops in Go 1.22 (go.dev)」看到的,原文在「Fixing For Loops in Go 1.22」這邊。
算是各種有 closure 的程式語言會遇到的經典問題,下面的 Golang 程式在撰寫時會預期輸出 a
、b
、c
(不保證順序),但實際上會輸出 c
、c
、c
,因為變數的 scope 設計原因:
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { go func() { fmt.Println(v) done <- true }() } // wait for all goroutines to complete before exiting for _ = range values { <-done } }
這在 JavaScript 也是個經典的問題,搜「javascript closure loop」就可以看到「JavaScript closure inside loops – simple practical example」這個例子,對應的答案則是有提到 ES6 的 let
因為改變了變數的 scope,可以用來解決這個問題。
剛好 let
也是 best practice 了,所以現在 JavaScript 這邊遇到這個問題的情況會少一些。
回到 Golang 這邊,Golang 是打算改變在 for loop 時,對應變數的 scope 來解決:
For Go 1.22, we plan to change for loops to make these variables have per-iteration scope instead of per-loop scope. This change will fix the examples above, so that they are no longer buggy Go programs; it will end the production problems caused by such mistakes; and it will remove the need for imprecise tools that prompt users to make unnecessary changes to their code.
另外因為這是 breaking compatibility 的改變 (畢竟有些程式碼是清楚知道這個特性而刻意這樣寫的),所以會有對應的措施,只有在有指定 Golang 1.22 或是更新的版本才會用新的 scope 編譯:
To ensure backwards compatibility with existing code, the new semantics will only apply in packages contained in modules that declare go 1.22 or later in their go.mod files. This per-module decision provides developer control of a gradual update to the new semantics throughout a codebase. It is also possible to use //go:build lines to control the decision on a per-file basis.
以位址來看的話,在一個loop (for i, v := range arr),裡面v的位址不等於arr[]中的任何一個元素位址,而是for迴圈當下建立出的另一個獨立位址。我想這樣就能很好的理解這個反直覺的地方,通常都會認為loop過程中&arr[i]==&n,然而從一開始就不是這樣。所以不僅是closure,就連一個簡單的channel send都有可能不小心寫錯,如下:
type S struct {
N int
}
func produce() <-chan S {
ch := make(chan S, 1)
go func() {
arr := []S{S{1}, S{2}, S{3}}
for i, n := range arr {
arr[i].N *= 3
ch <- n
}
close(ch)
}()
return ch
}