文章目录
好久没写了,打算今年做个Dig101系列,挖一挖技术背后的故事。
Dig101: dig more, simplified more and know more
golang常用的遍历方式,有两种: for 和 for-range。
而for-range使用中有些坑常会遇到,今天我们一起来捋一捋。
update: go 1.22中for遍历中变量已不再使用同一个变量共享, 新版本中文中for遍历变量共享问题已不存在。
0x01 遍历取不到所有元素指针?
如下代码想从数组遍历获取一个指针元素切片集合
1 | arr := [2]int{1, 2} |
答案是【取不到】
同样代码对切片[]int{1, 2}
或map[int]int{1:1, 2:2}
遍历也不符合预期。
问题出在哪里?
通过查看go编译源码可以了解到, for-range其实是语法糖,内部调用还是for循环,初始化会拷贝带遍历的列表(如array,slice,map),然后每次遍历的v
都是对同一个元素的遍历赋值。
也就是说如果直接对v
取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v
的值。对应伪代码如下:
1 | // len_temp := len(range) |
那么怎么改?
有两种
- 使用局部变量拷贝
v
1
2
3
4
5for _, v := range arr {
//局部变量v替换了v,也可用别的局部变量名
v := v
res = append(res, &v)
} - 直接索引获取原来的元素
1
2
3
4//这种其实退化为for循环的简写
for k := range arr {
res = append(res, &arr[k])
}
理顺了这个问题后边的坑基本都好发现了,来迅速过一遍
0x02 遍历会停止么?
1 | v := []int{1, 2, 3} |
答案是【会】,因为遍历前对v
做了拷贝,所以期间对原来v
的修改不会反映到遍历中
0x03 对大数组这样遍历有啥问题?
1 | //假设值都为1,这里只赋值3个 |
答案是【有问题】!遍历前的拷贝对内存是极大浪费啊
怎么优化?有两种
- 对数组取地址遍历
for i, n := range &arr
- 对数组做切片引用
for i, n := range arr[:]
反思题:对大量元素的slice和map遍历为啥不会有内存浪费问题?
(提示,底层数据结构是否被拷贝)
0x04 对大数组这样重置效率高么?
1 | //假设值都为1,这里只赋值3个 |
答案是【高】,这个要理解得知道go对这种重置元素值为默认值的遍历是有优化的, 详见go源码:memclrrange
1 | // Lower n into runtime·memclr if possible, for |
0x05 对map遍历时删除元素能遍历到么?
1 | var m = map[int]int{1: 1, 2: 2, 3: 3} |
答案是【不会】
map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置,
这样,如果删除的元素开始没被遍历到(上边once.Do
函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。
0x06 对map遍历时新增元素能遍历到么?
1 | var m = map[int]int{1:1, 2:2, 3:3} |
答案是【可能会】,输出中可能会有44
。原因同上一个, 可以用以下代码验证
1 | var createElemDuringIterMap = func() { |
0x07 这样遍历中起goroutine可以么?
1 | var m = []int{1, 2, 3} |
答案是【不可以】。预期输出0,1,2的某个组合,如012,210..
结果是222. 同样是拷贝的问题
怎么解决
- 以参数方式传入
1
2
3
4
5for i := range m {
go func(i int) {
fmt.Print(i)
}(i)
} - 使用局部变量拷贝
1
2
3
4
5
6for i := range m {
i := i
go func() {
fmt.Print(i)
}()
}
发现没,一个简单的for-range,仔细剖析下来也是有不少有趣的地方。
希望剖析后能让你更进一步的了解。
如有问题欢迎留言交流。
本文代码见 NewbMiao/Dig101-Go
参考
Go Range Loop Internals
Common Mistakes
go101: Arrays, Slices and Maps in Go
如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com 本文链接 : https://newbmiao.github.io/2020/01/03/dig101-golang-for-range.html