V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
newmiao
V2EX  ›  Go 编程语言

Dig101-Go 之 for-range 排坑指南

  •  
  •   newmiao · 2020-03-07 17:11:32 +08:00 · 1159 次点击
    这是一个创建于 1511 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Dig101: dig more, simplified more and know more

    golang 常用的遍历方式,有两种:for 和 for-range。 而 for-range 使用中有些坑常会遇到,今天我们一起来捋一捋。

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

    如下代码想从数组遍历获取一个指针元素切片集合

    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])
    }
    

    理顺了这个问题后边的坑基本都好发现了,来迅速过一遍

    0x02 遍历会停止么?

    v := []int{1, 2, 3}
    for i := range v {
        v = append(v, i)
    }
    

    答案是 [会] ,因为遍历前对v做了拷贝,所以期间对原来v的修改不会反映到遍历中

    0x03 对大数组这样遍历有啥问题?

    //假设值都为 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 遍历为啥不会有内存浪费问题? (提示,底层数据结构是否被拷贝)

    0x04 对大数组这样重置效率高么?

    //假设值都为 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.
    

    0x05 对 map 遍历时删除元素能遍历到么?

    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 %d\n", i, key)
                    delete(m, key)
                    break
                }
            }
        })
        fmt.Printf("%d%d ", i, m[i])
    }
    

    答案是 [不会] map 内部实现是一个链式 hash 表,为保证每次无序,初始化时会随机一个遍历开始的位置, 这样,如果删除的元素开始没被遍历到(上边once.Do函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。

    0x06 对 map 遍历时新增元素能遍历到么?

    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()
    }
    

    0x07 这样遍历中起 goroutine 可以么?

    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

    欢迎关注公众号:newbmiao,获取及时更新文章。

    推荐阅读:Dig101 系列,挖一挖技术背后的故事。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   892 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 22:00 · PVG 06:00 · LAX 15:00 · JFK 18:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.