循环中的闭包陷阱

在 go 中进行循环操作时, 对于批量任务, 大多都会用 goroutine 来并发处理. 下面代码使用了 goroutine + closure

1
2
3
4
5
for _, val := range values {
	go func() {
		fmt.Println(val)
	}()
}

相信熟悉的同学们已经看出来了, 这段代码是有问题的. 循环中变动的变量, 如果直接在闭包使用, 会导致 goroutine 处理时拿到的值可能是重复的值. 因为循环迭代使用了同一个变量, 所以每个闭包也都共享了这一个变量. 当闭包开始运行, 程序在 println 执行时打印了val的值, 但这个值在 goroutine 启动时可能已经被修改了, 或者说可能被修改了很多次. 可以用go vet来判断是否有此类似的问题.

为了绑定当前的val到每个启动后的闭包中, 需要在循环中新增变量. 一个方法是 将变量对闭包进行参数传递, 如下:

1
2
3
4
5
for _, val := range values {
	go func(val interface{}) {
		fmt.Println(val)
	}(val)
}

通过在闭包中传递参数, val变量在循环中被放在栈上,所以每一个循环中的变量在 goroutine 中都是有效的.

另外也可以在循环中定义局部变量来解决, 循环的每一次, 变量都会重新定义, 所以也不会在循环中被共享.

1
2
3
4
5
6
for i := range valslice {
	val := valslice[i]
	go func() {
		fmt.Println(val)
	}()
}

如果不在循环中定义新的变量, 可能会造成预想之外的行为. 这个行为在未来的 go 版本中可能会修改, 但在 version 1的大版本中不会做改动, 详见golang FAQ

如果闭包不通过 goroutine 来执行, 则不会有问题, 下面的代码会打印1 -> 10

1
2
3
4
5
for i := 1; i <= 10; i++ {
	func() {
		fmt.Println(i)
	}()
}

正确执行的原因是, 在每次循环迭代时, 匿名函数都会在拿到当前变量后同步执行

下面是另外一种类似的情况

1
2
3
4
5
6
7
for _, val := range values {
	go val.MyMethod()
}

func (v *val) MyMethod() {
        fmt.Println(v)
}

上面的代码依旧会只打印最后一个val的值, 原因同上, 要解决的话, 只需要在循环中新增一个变量即可

1
2
3
4
5
6
7
8
for _, val := range values {
        newVal := val
	go newVal.MyMethod()
}

func (v *val) MyMethod() {
        fmt.Println(v)
}

现在, 我们可以整理总结下, 循环中变量的使用规则

  1. 循环中如果不存在延迟执行, 则没有问题.
  2. 循环之外定义的变量, 如果只是读取也没有问题
  3. 可以通过临时变量保存循环的变量值, 以保证后续延迟执行的调用可靠

好吧, 之所以翻译这篇文章, 是因为遇到了异常, 直接看代码吧 go paly

1
2
3
4
5
6
bb := []ccc{{aa: 11, bb: 22}, {aa: 111, bb: 222},}
for _, i := range bb {
    go func(i *ccc) {
        fmt.Println(i)
    }(&i)
}

第五行, 传递的 i 的引用, 尽管遵从了闭包参数传递的规则, 但因为传送的是变量地址, 所以仍然导致问题. 如果去除引用传递, 则没有问题, 不管ccc的值是 struct 还是指针.

Reference:


comments powered by Disqus