循环变量的重用 #
for range 形式的循环语句,使用短变量声明的方式来声明循环变量。我们来看下面这个例子:
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
time.Sleep(time.Second * 10)
}
我们预期的输出结果可能是这样的:
0 1
1 2
2 3
3 4
4 5
而实际输出真的是这样:
4 5
4 5
4 5
4 5
4 5
初学者很可能会被 for range 语句中的短声明变量形式“迷惑”,简单地认为每次迭代都会重新声明两个新的变量 i 和 v。但事实上,这些循环变量在 for range 语句中仅会被声明一次,且在每次迭代中都会被重用。
我们可以将上面的 for range 语句做一个等价转换:
func main() {
var m = []int{1, 2, 3, 4, 5}
{
i, v := 0, 0
for i, v = range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
}
time.Sleep(time.Second * 10)
}
我们可以清晰地看到循环变量 i 和 v 在每次迭代时的重用。而 Goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样,变量 i、v 在主 Goroutine 和新启动的 Goroutine 之间实现了共享,而 i, v 值在整个循环过程中是重用的,仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 Goroutine 在等待 3 秒后进行输出的时候,输出的是 i, v 的最终值。
那么如何修改代码,可以让实际输出和我们最初的预期输出一致呢?我们可以为闭包函数增加参数,并且在创建 Goroutine 时将参数与 i、v 的当时值进行绑定,看下面的修正代码:
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func(i, v int) {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}(i, v)
}
time.Sleep(time.Second * 10)
}
运行修改后的例子代码,输出结果是这样的:
0 1
1 2
2 3
3 4
4 5
这回的输出结果与我们的预期就是一致的了。