Golang 將改變常見的 closure 地雷

在「Fixing for loops in Go 1.22 (go.dev)」看到的,原文在「Fixing For Loops in Go 1.22」這邊。

算是各種有 closure 的程式語言會遇到的經典問題,下面的 Golang 程式在撰寫時會預期輸出 abc (不保證順序),但實際上會輸出 ccc,因為變數的 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」這個例子,對應的答案則是有提到 ES6let 因為改變了變數的 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.

One thought on “Golang 將改變常見的 closure 地雷”

  1. 以位址來看的話,在一個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
    }

Leave a Reply

Your email address will not be published. Required fields are marked *