在并发的goroutine中遇到这个问题,还以为是goroutine的问题,或者Go闭包的问题, 一层一层分析下来,才知道是Go本身迭代变量的问题,想起了PHP的foreach陷阱【PHP引用理解之神奇的foreach面试题】,不仅感慨深入掌握一门语言何其不易呀
先说结论:Go在循环体中,每次遍历的变量是同一个,指向同样的内存地址,这是语言本身的一个陷阱
问题先行,我们先来看看问题
// 匿名函数测试 func TestAnonymous2(t *testing.T) { defer func() { if r := recover(); r != nil { log.Fatal(r) } }() fmt.Println(runtime.NumCPU()) codeNameMap := map[string]string{ "1000": "滴滴出行", "2000": "Uber", "3000": "高德打车", } wg := &sync.WaitGroup{} for code, name := range codeNameMap { wg.Add(1) go func() { defer wg.Done() res := fmt.Sprintf("goroutine two. code: %s; name: %s", code, name) fmt.Println(res) }() } wg.Wait() fmt.Println("over") } // 执行结果 8 goroutine two. code: 3000; name: 高德打车 goroutine two. code: 3000; name: 高德打车 goroutine two. code: 3000; name: 高德打车 over
如果你也遇到类似问题,困惑很久,那么这篇笔记很适合你,要有耐心读到最后,除了解惑,还会领悟很多
一、陷阱现象
1、描述
func TestIterator1(t *testing.T) { for i := 0; i < 3; i++ { fmt.Println(&i) } } // 执行结果 0x140000981c0 0x140000981c0 0x140000981c0
2、原因分析
循环迭代的变量共享共一块内存地址
3、地址是同一个,为啥值不相同
func TestIterator2(t *testing.T) { for i := 0; i < 3; i++ { fmt.Println(i) } } // 执行结果 0 1 2
打印循环变量某一刻的值【某一次遍历的值】
func TestIterator3(t *testing.T) { out := []int{} for i := 0; i < 3; i++ { out = append(out, i) } fmt.Println(out) } // 执行结果 [0 1 2]
记录循环变量某一刻的值,最后统一打印【某一次遍历的值】
二、陷阱场景之循环变量中使用引用
1、现象
func TestIterator4(t *testing.T) { var out []*int for i := 0; i < 3; i++ { out = append(out, &i) } fmt.Println("values: ", *out[0], *out[1], *out[2]) fmt.Println("addresses: ", out[0], out[1], out[2]) } // 执行结果 values: 3 3 3 addresses: 0x140000981c0 0x140000981c0 0x140000981c0
2、解法
每次遍历的变量赋值给一个新的变量【一般用同名变量】,注意line4
func TestIterator5(t *testing.T) { var out []*int for i := 0; i < 3; i++ { i := i out = append(out, &i) } fmt.Println("values: ", *out[0], *out[1], *out[2]) fmt.Println("addresses: ", out[0], out[1], out[2]) }
三、陷阱场景之循环变量中使用goroutines
1、现象
func TestIterator6(t *testing.T) { for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() } time.Sleep(10 * time.Millisecond) } // 执行结果 3 3 3
2、原理
在goroutine执行之前,匿名函数中变量已经被修改了
3、解法a
func TestIterator7(t *testing.T) { for i := 0; i < 3; i++ { i := i go func() { fmt.Println(i) }() } time.Sleep(10 * time.Millisecond) } // 执行结果 2 1 0
4、解法b
func TestIterator8(t *testing.T) { for i := 0; i < 3; i++ { go func(i int) { fmt.Println(i) }(i) } time.Sleep(10 * time.Millisecond) } // 执行结果 2 1 0
5、不使用goroutine,结果是符合预期的
因为也是打印了某一次遍历的值
func TestIterator9(t *testing.T) { for i := 0; i < 3; i++ { func() { fmt.Println(i) }() } } // 执行结果 0 1 2
四、陷阱场景之循环变量中使用defer
1、现象
func TestIterator10(t *testing.T) { for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() } } // 执行结果 3 3 3
2、原理
加defer,遍历完了才执行,匿名函数中变量已经被修改了
3、解法a
func TestIterator11(t *testing.T) { for i := 0; i < 3; i++ { i := i defer func() { fmt.Println(i) }() } } // 执行结果 2 1 0
4、解法b
func TestIterator12(t *testing.T) { for i := 0; i < 3; i++ { defer func(i int) { fmt.Println(i) }(i) } } // 执行结果 2 1 0
五、陷阱场景之循环变量中使用匿名函数
1、现象
func TestIterator13(t *testing.T) { var squares []func() int for i := 0; i < 3; i++ { squares = append(squares, func() int { return i * i }) } for _, square := range squares { res := square() fmt.Println(res) } } // 执行结果 9 9 9
2、原理
因为循环变量是共享的,打印的不是某一次的值,最后统一输出,值就会被改写,【笼统的说法是匿名函数是引用类型,很容易让人迷惑】
比如:《Go圣经》5.6.1. 警告:捕获迭代变量
3、解法a
func TestIterator14(t *testing.T) { var squares []func() int for i := 0; i < 3; i++ { i := i squares = append(squares, func() int { return i * i }) } for _, square := range squares { res := square() fmt.Println(res) } }
六、小结
1、对于for range上面的问题同理
2、问题的本质,还是看记录打印的是某一次的值,还是最后统一记录
3、每种语言都有易错的陷阱,想起了PHP的foreach,一定要把陷阱了熟于胸,才行之不乱
PHP引用理解之神奇的foreach面试题【本站】
七、参考
1、https://github.com/golang/go/wiki/CommonMistakes【Github, Go官方common mistakes】
2、https://go.dev/doc/faq#closures_and_goroutines
3、How golang’s “defer” capture closure’s parameter?【stackoverflow经典问题,关于defer】
4、Go internals: capturing loop variables in closures
5、Go中被闭包捕获的变量何时会被回收