Go 语言 for-range 的两个坑,你踩过吗?

2023-07-09 22:19:31 浏览数 (2)

坑一:迭代时协程引用索引和值

先看看下面的例子,你知道最终输出的结果是什么吗?

代码语言:go复制
package main

import (
	"fmt"
	"time"
)

func main() {
	var m = []int{1, 3, 5}
	for i, v := range m {
		go func() {
			fmt.Println(i, v)
		}()
	}
	time.Sleep(time.Second)
}

不知道的同学,大家可以在 https://play.golang.org/ 这里尝试运行一下。

正确答案是:

代码语言:txt复制
2 5
2 5
2 5

这是为啥?三个 goroutine 都是输出了最后迭代的索引和值。

我觉得,理解清楚以下两点就可以了:

  1. 闭包内引用了外部变量 i 和 v,三个协程都引用了
  2. 协程运行时,循环可能已经结束了

要想解决这个问题,可以改成闭包传参的形式:

代码语言:go复制
func main() {
	var m = []int{1, 3, 5}
	for i, v := range m {
		go func(_i, _v int) {
			fmt.Println(_i, _v)
		}(i, v)
	}
	time.Sleep(time.Second)
}

也可以让每一轮循环都用新的变量:

代码语言:go复制
func main() {
	var m = []int{1, 3, 5}
	for i, v := range m {
		_i := i
		_v := v
		go func() {
			fmt.Println(_i, _v)
		}()
	}
	time.Sleep(time.Second)
}

运行结果就符合预期了:

代码语言:txt复制
0 1
1 3
2 5
坑二:迭代时值为原先迭代对象的拷贝
代码语言:go复制
package main

import (
	"fmt"
)

func main() {
	var m = []int{1, 3, 5}
	for i, v := range m {
		if i == 1 {
			v = 2
		}
		fmt.Println(i, v)
	}
	fmt.Println(m)
}

这个输出是啥?

代码语言:txt复制
0 1
1 2 // 明明改成 2 了,
2 5
[1 3 5] // 这里还是 3 ?

那是因为,Go 会在 range 循环中自动为遍历的对象创造一个副本,可以理解为一个值拷贝,如果真的想修改原数组,你得这样写:

代码语言:go复制
func main() {
	var m = []int{1, 3, 5}
	for i, v := range m {
		if i == 1 {
			m[i] = 2 // 改成 m[i]
		}
		fmt.Println(i, v)
	}
	fmt.Println(m)
}

程序输出是这样:

代码语言:txt复制
0 1
1 3 // 由于是值拷贝,所以改了原来的 m[i] 不影响 v 的值,m[i]=2,v=3.
2 5
[1 2 5] // 可以看到已经改成 2 了

也不算是什么大坑,理解了感觉和 PHP 的 foreach 语法差不多。

文章来源于本人博客,发布于 2019-06-16,原文链接:https://imlht.com/archives/187/

0 人点赞