赞
踩
打算今年做个Dig101系列,挖一挖技术背后的故事。
文章首发:公众号 newbmiao
Dig101: dig more, simplified more and know more
golang常用的遍历方式,有两种: for 和 for-range。 而for-range使用中有些坑常会遇到,今天我们一起来捋一捋。
如下代码想从数组遍历获取一个指针元素切片集合
- arr := [2]int{1, 2}
- res := []*int{}
- for _, v := range arr {
- res = append(res, &v)
- }
- //expect: 1 2
- fmt.Println(*res[0],*res[1])
- //but output: 2 2
答案是【取不到】 同样代码对切片[]int{1, 2}
或map[int]int{1:1, 2:2}
遍历也不符合预期。 问题出在哪里?
通过查看go编译源码可以了解到, for-range其实是语法糖,内部调用还是for循环,初始化会拷贝带遍历的列表(如array,slice,map),然后每次遍历的v
都是对同一个元素的遍历赋值。 也就是说如果直接对v
取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v
的值。对应伪代码如下:
- // len_temp := len(range)
- // range_temp := range
- // for index_temp = 0; index_temp < len_temp; index_temp++ {
- // value_temp = range_temp[index_temp]
- // index = index_temp
- // value = value_temp
- // original body
- // }
那么怎么改? 有两种 - 使用局部变量拷贝v
- for _, v := range arr {
- //局部变量v替换了v,也可用别的局部变量名
- v := v
- res = append(res, &v)
- }
- //这种其实退化为for循环的简写
- for k := range arr {
- res = append(res, &arr[k])
- }
理顺了这个问题后边的坑基本都好发现了,来迅速过一遍
- v := []int{1, 2, 3}
- for i := range v {
- v = append(v, i)
- }
答案是【会】,因为遍历前对v
做了拷贝,所以期间对原来v
的修改不会反映到遍历中
- //假设值都为1,这里只赋值3个
- var arr = [102400]int{1, 1, 1}
- for i, n := range arr {
- //just ignore i and n for simplify the example
- _ = i
- _ = n
- }
答案是【有问题】!遍历前的拷贝对内存是极大浪费啊 怎么优化?有两种 - 对数组取地址遍历 for i, n := range &arr
- 对数组做切片引用 for i, n := range arr[:]
反思题:对大量元素的slice和map遍历为啥不会有内存浪费问题? (提示,底层数据结构是否被拷贝)
- //假设值都为1,这里只赋值3个
- var arr = [102400]int{1, 1, 1}
- for i, _ := range &arr {
- arr[i] = 0
- }
答案是【高】,这个要理解得知道go对这种重置元素值为默认值的遍历是有优化的, 详见go源码:memclrrange
- // Lower n into runtime·memclr if possible, for
- // fast zeroing of slices and arrays (issue 5373).
- // Look for instances of
- //
- // for i := range a {
- // a[i] = zero
- // }
- //
- // in which the evaluation of a is side-effect-free.
- var m = map[int]int{1: 1, 2: 2, 3: 3}
- //only del key once, and not del the current iteration key
- var o sync.Once
- for i := range m {
- o.Do(func() {
- for _, key := range []int{1, 2, 3} {
- if key != i {
- fmt.Printf("when iteration key %d, del key %dn", i, key)
- delete(m, key)
- break
- }
- }
- })
- fmt.Printf("%d%d ", i, m[i])
- }
答案是【不会】 map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置, 这样,如果删除的元素开始没被遍历到(上边once.Do
函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。
- var m = map[int]int{1:1, 2:2, 3:3}
- for i, _ := range m {
- m[4] = 4
- fmt.Printf("%d%d ", i, m[i])
- }
答案是【可能会】,输出中可能会有44
。原因同上一个, 可以用以下代码验证
- var createElemDuringIterMap = func() {
- var m = map[int]int{1: 1, 2: 2, 3: 3}
- for i := range m {
- m[4] = 4
- fmt.Printf("%d%d ", i, m[i])
- }
- }
- for i := 0; i < 50; i++ {
- //some line will not show 44, some line will
- createElemDuringIterMap()
- fmt.Println()
- }
- var m = []int{1, 2, 3}
- for i := range m {
- go func() {
- fmt.Print(i)
- }()
- }
- //block main 1ms to wait goroutine finished
- time.Sleep(time.Millisecond)
答案是【不可以】。预期输出0,1,2的某个组合,如012,210.. 结果是222. 同样是拷贝的问题 怎么解决 - 以参数方式传入
- for i := range m {
- go func(i int) {
- fmt.Print(i)
- }(i)
- }
- for 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。