今天看到一个提问,作者在for循环中执行了异步任务,这些异步任务通过闭包的方式读取循环语句range中的参数,但这些任务读到的闭包参数值都相同,这是为什么呢?

首先来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
func ForRange() {
nums := []int{1, 2, 3}
for _, num := range nums {
fmt.Println(&num)
}
}

// 0xc000014248
// 0xc000014248
// 0xc000014248

可以看到,在 for-range 循环中,循环变量 num 的地址是不随循环次数改变的。

问题

了解到了这一点,我们再来看提问者遇到的问题。首先,我们把问题抽象成如下的简单代码。这段代码在 for-range 循环中执行了一个异步任务,并通过闭包的方式读取到了循环变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func ForRangeAsync() {
type Person struct {
Name string
}

group := []Person{{"li"}, {"zhao"}}

var wg sync.WaitGroup

for _, p := range group {
wg.Add(1)

go func() {
defer wg.Done()
fmt.Println("name =", p.Name)
}()
}

wg.Wait()
}

预期中这段代码会打印 group 切片,但实际的执行结果却和我们的想法不同。

1
2
name = zhao
name = zhao

结合上面的例子我们不难发现问题的原因所在。

由于循环变量 p 有独立的地址,每次循环都会共享这个地址。

当异步任务开始执行的时候,异步任务通过闭包读取到的都是循环已经结束时 p 的值。

此时 p 中存储的是切片中的最后一个值,因此异步任务打印的结果都是相同的。

解决方法一

其实,只要稍作调整,在本次循环结束之前等待异步任务执行完毕,就能读到对应的 p 值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func ForRangeAsync() {
type Person struct {
Name string
}

group := []Person{{"li"}, {"zhao"}}

var wg sync.WaitGroup

for _, p := range group {
wg.Add(1)

go func() {
defer wg.Done()
fmt.Println("name =", p.Name)
}()

wg.Wait()
}
}

// name = li
// name = zhao

不过这种修改不符合我们想要利用异步提高执行效率的目的,还有没有别的方法呢?

解决方法二

另一个更简单的方法就是每次循环中声明一个新的局部变量保存此次循环下的 p 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func ForRangeAsyncFix2() {
type Person struct {
Name string
}

group := []Person{{"li"}, {"zhao"}}

var wg sync.WaitGroup

for _, p := range group {
p := p
wg.Add(1)

go func() {
defer wg.Done()
fmt.Println("name =", p.Name)
}()
}
wg.Wait()
}

// name = zhao
// name = li

相关链接