循环变量的重用

循环变量的重用

循环变量的重用 #

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

这回的输出结果与我们的预期就是一致的了。

Viewpoint #

From #

19|控制结构:Go的for循环,仅此一种