TOC

Go 1.21 for 语义变更

仔细观察下面的例子就能知道问题在哪里了:

例子 1

package main

import "fmt"

func main() {
    items := []int{1, 2, 3}

    {
        var all []*int
        for _, item := range items {
            all = append(all, &item)
        }
        fmt.Printf("%+v\n", all)
        // [0xc00008c018 0xc00008c018 0xc00008c018]
        // 输出的都是最后一个值!!!
        for _, i := range all {
            fmt.Printf("%+v, %+v\n", i, *i)
        }
    }

    // fix it:
    {
        var all []*int
        for _, item := range items {
            item := item // 重点
            all = append(all, &item)
        }
        for _, i := range all {
            fmt.Printf("%+v, %+v\n", i, *i)
        }
    }
}

例子 2

package main

import "fmt"

func main() {
    {
        var prints []func()
        for _, v := range []int{1, 2, 3} {
            prints = append(prints, func() { fmt.Println(v) })
        }
        for _, print := range prints {
            print()
        }
    }
    // 输出的是 3 3 3,而不是 1,2,3,Why?

    // fix it:
    {
        var prints []func()
        for _, v := range []int{1, 2, 3} {
            v := v // 重点
            prints = append(prints, func() { fmt.Println(v) })
        }
        for _, print := range prints {
            print()
        }
    }
}

例子 3

package main

import (
    "fmt"
    "sync"
)

func main() {
    items := []int{1, 2, 3}

    {
        wg := sync.WaitGroup{}
        for _, v := range items {
            wg.Add(1)
            go func() {
                // 会提示:loop variable v captured by func literal
                fmt.Println(v)
                wg.Done()
            }()
        }
        wg.Wait()
    }

    // fix it:
    {
        wg := sync.WaitGroup{}
        for _, v := range items {
            wg.Add(1)
            v := v // 重点
            go func() {
                fmt.Println(v)
                wg.Done()
            }()
        }
        wg.Wait()
    }
}

这个例子可以改写成:

package main

import (
    "fmt"
)

func main() {
    items := []int{1, 2, 3}
    done := make(chan bool)
    {
        for _, v := range items {
            go func() {
                // 会提示:loop variable v captured by func literal
                fmt.Println(v)
                done <- true
            }()
        }
        for _ = range items {
            <-done
        }
    }

    // fix it:
    {
        for _, v := range items {
            v := v // 重点
            go func() {
                fmt.Println(v)
                done <- true
            }()
        }
        for _ = range items {
            <-done
        }
    }
}

我的理解

根据 Go 的设计思想,花括号内是一个独立的作用域,for 循环每一次也应该是独立的。
当前这次循环中的变量和上一次循环的变量应该是不一样的。
但实际上,根据运行结果来看,他们的地址是一样的。

闭包函数应该也是这样的,去原来的位置读相关变量,但是之前的位置写入了新的值。

这个设计是一个大坑,对于新人非常不友好。

从 1.21 开始支持根据环境变量 GOEXPERIMENT=loopvar 来启用新的订正版语义。
从 1.22 开始正式修改 for 语义。

Russ Cox 认为当前语义的代价很大,出错的频率高于正确的频率。

但是这次变更 for 语句语义的决定和之前的承诺有冲突(保证兼容性的基础就是语义不会被重新定义)。
但是好在 Go 已经为这种情况做好了足够的准备,即,go 支持在同一批编译中,按照不同包的 mod 文件中声明的 go 版本来对体现不同的语法特性。
就比如 A 包需要 1.22,然后依赖 B 包,需要 1.18,那么 A 包中的代码按照新的定义来,B 包中的代码按照旧的定义来。

https://github.com/golang/go/discussions/56010