仔细观察下面的例子就能知道问题在哪里了:
例子 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 包中的代码按照旧的定义来。