Go语言之for-range实现原理及排坑

1,936 阅读3分钟

1. for-range实现原理

1.1. 对于slice

for_temp := range
len_temp := len(for_temp)
for index_temp = 0; index_temp < len_temp; index_temp++ {
    value_temp = for_temp[index_temp]
    index = index_temp
    value = value_temp
    original body
}

1.2. 对于数组

// Lower a for range over an array.
// The loop we generate:
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
}

1.3. 对于字符串

// Lower a for range over a string.
// The loop we generate:
len_temp := len(range)
var next_index_temp int
for index_temp = 0; index_temp < len_temp; index_temp = next_index_temp {
    value_temp = rune(range[index_temp])
    if value_temp < utf8.RuneSelf {
        next_index_temp = index_temp + 1
    } else {
        value_temp, next_index_temp = decoderune(range, index_temp)
    }
    index = index_temp
    value = value_temp
    // original body
}

1.4. 对于map

// Lower a for range over a map.
// The loop we generate:
// var hiter map_iteration_struct
for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
    index_temp = *hiter.key
    value_temp = *hiter.val
    index = index_temp
    value = value_temp
    original body
}

1.5. 对于channel

// Lower a for range over a channel.
// The loop we generate:
for {
    index_temp, ok_temp = <-range
    if !ok_temp {
        break
    }
    index = index_temp
    original body
}

2. for-range排坑

2.1. k,v地址保持不变

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

由于在for-range循环过程中,v的地址保持不变,而v地址对应的值时最后一次循环对应的v的值。故修正可采用如下方法:

// 方法1
for _, v := range arr {
    //局部变量v替换了v,也可用别的局部变量名
    v := v
    res = append(res, &v)
}

// 方法2
//这种其实退化为for循环的简写
for k := range arr {
    res = append(res, &arr[k])
}

同样在for-rangegoroutine闭包捕获的代码也是经常出现的bug

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)


// 修改方法1——以参数方式传入
for i := range m {
    go func(i int) {
        fmt.Print(i)
    }(i)
}

// 修改方法2——使用局部变量拷贝
for i := range m {
    i := i
    go func() {
        fmt.Print(i)
    }()
}

2.2. 在循环过程中对切片进行append操作

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

咋一看,发现遍历是一个死循环的结果,回过头看上面slice实现的for-range,它把遍历的对象和遍历的长度都拷贝了一份。因此不会出现死循环。即最后v的结果为{1,2,3,0,1,2}

2.3. 对大数组遍历问题

//假设值都为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[:]

2.4. 对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])
}

2.5. 对map遍历时新增元素会被遍历到吗——可能会

// 测试1
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])
}


// 测试2
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()
}

2.6. range操作的对象都是在复制的对象中操作,而不是原对象上

package main
import "fmt"
func main(){
    a := [3]int {1, 2, 3}
    for i, v := range a{ //i,v从a复制的对象里提取出
        if i == 0{
            a[1], a[2] = 200, 300
            fmt.Println(a) //输出[1 200 300]
        }
        a[i] = v + 100 //v是复制对象里的元素[1, 2, 3]
    }
    fmt.Println(a)  //输出[101, 102, 103]
}

由于数组是值拷贝,即range的对象是[1,2,3]。而中间过程中对原对象做任何改变,都不会应该拷贝对象的值。

package main
import "fmt"
func main(){
    a := []int {1, 2, 3} //改成slice
    for i, v := range a{ 
        if i == 0{
            a[1], a[2] = 200, 300
            fmt.Println(a) //[1 200 300]
        }
        a[i] = v + 100 
    }
    fmt.Println(a)  //[101 300 400]
}

由于切片是地址拷贝,即range拷贝出来的值和原来变量的值都是指向了同一个对象,当一个地方改变了对象的值,另外一个地方来访问对象的值时也会随之发生变化。