for-range排坑指南

打印 上一主题 下一主题

主题 878|帖子 878|积分 2634

0x01 遍历取不到所有元素指针?

如下代码想从数组遍历获取一个指针元素切片集合
  1. arr := [2]int{1, 2}
  2. res := []*int{}
  3. for _, v := range arr {
  4.     res = append(res, &v)
  5. }
  6. //expect: 1 2
  7. fmt.Println(*res[0],*res[1])
  8. //but output: 2 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)
  2. // range_temp := range
  3. // for index_temp = 0; index_temp < len_temp; index_temp++ {
  4. //     value_temp = range_temp[index_temp]
  5. //     index = index_temp
  6. //     value = value_temp
  7. //     original body
  8. //   }
复制代码
那么怎么改? 有两种 - 使用局部变量拷贝v
  1. for _, v := range arr {
  2.     //局部变量v替换了v,也可用别的局部变量名
  3.     v := v
  4.     res = append(res, &v)
  5. }
复制代码

  • 直接索引获取原来的元素
  1. //这种其实退化为for循环的简写
  2. for k := range arr {
  3.     res = append(res, &arr[k])
  4. }
复制代码
理顺了这个问题后边的坑基本都好发现了,来迅速过一遍
0x02 遍历会停止么?
  1. v := []int{1, 2, 3}
  2. for i := range v {
  3.     v = append(v, i)
  4. }
复制代码
答案是【会】,因为遍历前对v做了拷贝,所以期间对原来v的修改不会反映到遍历中
0x03 对大数组这样遍历有啥问题?
  1. //假设值都为1,这里只赋值3个
  2. var arr = [102400]int{1, 1, 1}
  3. for i, n := range arr {
  4.     //just ignore i and n for simplify the example
  5.     _ = i
  6.     _ = n
  7. }
复制代码
答案是【有问题】!遍历前的拷贝对内存是极大浪费啊 怎么优化?有两种 - 对数组取地址遍历 for i, n := range &arr - 对数组做切片引用 for i, n := range arr[:]
反思题:对大量元素的slice和map遍历为啥不会有内存浪费问题? (提示,底层数据结构是否被拷贝)
0x04 对大数组这样重置效率高么?
  1. //假设值都为1,这里只赋值3个
  2. var arr = [102400]int{1, 1, 1}
  3. for i, _ := range &arr {
  4.     arr[i] = 0
  5. }
复制代码
答案是【高】,这个要理解得知道go对这种重置元素值为默认值的遍历是有优化的, 详见go源码:memclrrange
  1. // Lower n into runtime·memclr if possible, for
  2. // fast zeroing of slices and arrays (issue 5373).
  3. // Look for instances of
  4. //
  5. // for i := range a {
  6. //  a[i] = zero
  7. // }
  8. //
  9. // in which the evaluation of a is side-effect-free.
复制代码
0x05 对map遍历时删除元素能遍历到么?
  1. var m = map[int]int{1: 1, 2: 2, 3: 3}
  2. //only del key once, and not del the current iteration key
  3. var o sync.Once
  4. for i := range m {
  5.     o.Do(func() {
  6.         for _, key := range []int{1, 2, 3} {
  7.             if key != i {
  8.                 fmt.Printf("when iteration key %d, del key %d\n", i, key)
  9.                 delete(m, key)
  10.                 break
  11.             }
  12.         }
  13.     })
  14.     fmt.Printf("%d%d ", i, m[i])
  15. }
复制代码
答案是【不会】 map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置, 这样,如果删除的元素开始没被遍历到(上边once.Do函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。
0x06 对map遍历时新增元素能遍历到么?
  1. var m = map[int]int{1:1, 2:2, 3:3}
  2. for i, _ := range m {
  3.     m[4] = 4
  4.     fmt.Printf("%d%d ", i, m[i])
  5. }
复制代码
答案是【可能会】,输出中可能会有44。原因同上一个, 可以用以下代码验证
  1. var createElemDuringIterMap = func() {
  2.     var m = map[int]int{1: 1, 2: 2, 3: 3}
  3.     for i := range m {
  4.         m[4] = 4
  5.         fmt.Printf("%d%d ", i, m[i])
  6.     }
  7. }
  8. for i := 0; i < 50; i++ {
  9.     //some line will not show 44, some line will
  10.     createElemDuringIterMap()
  11.     fmt.Println()
  12. }
复制代码
0x07 这样遍历中起goroutine可以么?
  1. var m = []int{1, 2, 3}
  2. for i := range m {
  3.     go func() {
  4.         fmt.Print(i)
  5.     }()
  6. }
  7. //block main 1ms to wait goroutine finished
  8. time.Sleep(time.Millisecond)
复制代码
答案是【不可以】。预期输出0,1,2的某个组合,如012,210.. 结果是222. 同样是拷贝的问题 怎么解决 - 以参数方式传入
  1. for i := range m {
  2.     go func(i int) {
  3.         fmt.Print(i)
  4.     }(i)
  5. }
复制代码

  • 使用局部变量拷贝
  1. for i := range m {
  2.     i := i
  3.     go func() {
  4.         fmt.Print(i)
  5.     }()
  6. }
  7. 引用:<a target="_blank" href="https://zhuanlan.zhihu.com/p/105435646" rel="noopener">Dig101 - Go之for-range排坑指南 - 知乎 (zhihu.com)</a>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美丽的神话

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表