Go-for range

作者: 链人成长chainerup | 来源:发表于2019-09-24 12:52 被阅读0次

    Go中 有关循环,有两种,一种是for i :=0;i < len(x); i++ 的经典模式,另外一种是for k, v := range xxx。在用第二种方式时,有一些坑,这儿简单总结一下,希望对跟我一样有疑惑的朋友 有所帮助~

    1 for range 支持的数据类型

    for range 目前支持slice、map、string以及channel。 在每一种的使用过程中编译器都会对其做转换。接着往下看

    2 for range 应用与原理

    2.1 slice 与 array

    为了方便讲解,下文的 默认操作例子 for k, v := range a {}

    slice 跟 array 的for range 操作,会被编译器转换成经典for循环模式。
    编译之后,大体是这样的:

    ha := a
    hv1 := 0
    hn := len(ha)
    v1 := hv1
    for ; hv1 < hn; hv1++ {
        // ...
    }
    

    在转换之前,首先是进行了slice 或者array进行了拷贝,这样在实际遍历过程中,对v 的操作就不是对原slice/array操作了。如果需要影响原slice的内容,则需要操作&a[k] 了。

    举个例子直观看一下:

    func main() {
      a := []int{1,2,3}
        for _, v := range a {
      // 这儿的v 是 a的副本的value值。所以这样不会改变a的值
            v++
        }
        fmt.Println(a) // [1 2 3]
        // 如果想改变原来数组中的内容,其实可以直接操作原slice 地址
        for k, v := range a {
            a[k] = v + 1
        }
        fmt.Println(a) // [2 3 4]
    }
    

    再来个例子:

    func main() {
        a := []int{1,2,3}
        for _, v := range a {
        // v 是a副本的值,所以只有3个哦~
            a = append(a, v)
        }
        fmt.Println(a) // 1 2 3 1 2 3
    }
    

    2.2 map

    2.2.1 map for range 的无序性

    map 的 for range 操作,编译之后会被展开为 mapiterinitmapiternext
    编译之后,大体是这样的:

    ha := a
    hit := hiter(n.Type)
    th := hit.Type
    mapiterinit(typename(t), ha, &hit)
    for ; hit.key != nil; mapiternext(&hit) {
        key := *hit.key
        val := *hit.val
    }
    

    mapiterinit 的主要用途是:

    随机找一个桶作为遍历的开始(这也是为什么每次for range同一个map结果不一样的原因

    mapiternext 的主要用途是:

    • 遍历桶中的数据
    • 当前桶中数据遍历完毕,寻找下一个桶
    • 所有桶都遍历完毕,则结束。

    注意:每次for range map ,都会产生不同的顺序。

    2.2.2 map for range 过程中添加、删除元素

    我们再看一个 写了个例子:

    func main() {
      amap := map[string]int{"zhangpeng":3, "chris": 1, "lmm" : 2}
        for k, v := range amap {
            fmt.Println(k)
            //time.Sleep(time.Microsecond * 50)
            k = k + "-1"
            amap[k] = v
        }
        fmt.Println(amap)
    }
    

    发现每次执行的结论是不一样的。
    这是为什么呢?

    • 如果 map 中的元素在还没有被遍历到时就被移除了,后续的迭代中这个元素就不会再出现
    • 如果 map 中的元素是在迭代过程中被添加的,那么在后续的迭代这个元素可能出现也可能被跳过。

    2.3 string

    字符串的遍历与数组和哈希表非常相似,只是在遍历的过程中会获取字符串中索引对应的字节,然后将字节转换成 rune,我们在遍历字符串时拿到的值都是 rune 类型的变量,其实类似 for i, r := range s {} 的结构都会被转换成如下的形式:

    ha := s
    for hv1 := 0; hv1 < len(ha); {
        hv1t := hv1
        hv2 := rune(ha[hv1])
        if hv2 < utf8.RuneSelf {
            hv1++
        } else {
            hv2, hv1 = decoderune(h1, hv1)
        }
        v1, v2 = hv1t, hv2
    }
    

    如果当前的 rune 是 ASCII 的,那么只会占用一个字节长度,这时只需要将索引加一,但是如果当前的 rune 占用了多个字节就会使用 decoderune 进行解码。

    举个例子:

    func main() {
      astr := "hello 中国"
        for k, v := range astr {
            fmt.Println(k, v, string(v))
        }
    }
    

    结果是:

    0 104 h
    1 101 e
    2 108 l
    3 108 l
    4 111 o
    5 32  
    6 20013 中
    9 22269 国
    

    如果使用经典的for 循环,遍历string, 就会出现乱码问题了。

    func main() {
      astr := "hello 中国"
        for i := 0; i < len(astr); i++ {
            fmt.Println(i, astr[i], string(astr[i]))
        }
    }
    

    结论是这样的:

    0 104 h
    1 101 e
    2 108 l
    3 108 l
    4 111 o
    5 32  
    6 228 ä
    7 184 ¸
    8 173 ­
    9 229 å
    10 155 
    11 189 ½
    

    2.4 channel

    for v := range ch {} 编译之后大体是这样的:

    ha := a
    hv1, hb := <-ha
    for ; hb != false; hv1, hb = <-ha {
        v1 := hv1
        hv1 = nil
        // ...
    }
    

    循环会使用 <-ch 从管道中取出等待处理的值,这个操作会调用 chanrecv2 并阻塞当前的协程,当 chanrecv2 返回时会根据 hb 来判断当前的值是否存在,如果不存在就意味着当前的管道已经被关闭了,在正常情况下都会为 v1 赋值并清除 hv1 中的数据,然后会陷入下一次的阻塞等待接受新的数据。

    这部分目前还没有遇到坑,简单举个例子吧

    func main() {
        queue := make(chan string, 2)
        queue <- "one"
        queue <- "two"
        //close(queue)
        for elem := range queue {
            fmt.Println(elem)
        }
    }
    

    3 小结

    本文总结了for range 在Go中的使用,以及一些遇到的坑。希望对你有所帮助~

    4 参考文献

    知其然,知其所以然 https://draveness.me/golang/keyword/golang-for-range.html
    map在循环过程中添加、删除元素会发生什么?https://www.jianshu.com/p/35c2662b5b57

    5 其他

    本文是《循序渐进go语言》的第十篇-《Go-for range》。
    如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

    相关文章

      网友评论

        本文标题:Go-for range

        本文链接:https://www.haomeiwen.com/subject/rfivuctx.html