循环之Go语言&for range使用中的坑

978 阅读4分钟

Go语言的循环只有一种,就是以for开头。不像其他语言有多种形式。Go语言的for循环有3种形式:

1. for init; condition; post { }  // 类似c语言for循环 init可以是一个简短的变量声明,一个递增或赋值语句,或者是一个函数调用!
2. for condition { }           // 类似c语言while
3. for {}                     // 类似c语言for(;;)

为什么说类似C语言呢,下图是Go的“家谱”

image.png

for range 的使用

for循环的range格式可以对 slice、map、数组、字符串等进行迭代循环。

for range 在使用中的坑

for range 虽然使用方便,但是一不注意就容易掉坑里,需要注意以下几个方面:

1. for range 取不到所有元素的地址
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
    res = append(res, &v)  
 } 
fmt.Println(*res[0],*res[1])  / /expect: 1 2

以为会是 1 2 实际是 2 2。 下面就来分析一下:

for range 其实是一种语法糖,对应的go编译源码链接

为什么会这样呢?本质原因就是变量v一直是同一个,地址是一样的,你也可以试一下,直接对v取地址,最终都是相同的一个地址,所以后面取出来的值会把前面的值给覆盖掉了。v地址最后的值会是最后一个赋值给v的值,这里为2。

解决办法:

  • 使用局部变量拷贝, 这样每次都对v1进行初始化赋值,其地址是不同的。
for _, v := range arr { 
    v1 := v
    res = append(res, &v1)
}
  • 直接使用索引获取原来的元素
for k := range arr {
    res = append(res, &arr[k])
}

这让我想到一个相似的东西,java当中dfs使用到双层列表的时候,在把单层列表加入的时候都会使用这样的语句:res.add(new ArrayList<>(track)),也就是在add的时候需要做一层拷贝,否则到最后全都是空列表,因为dfs遍历完成以后,回到了根节点,成为了空列表。引用传递需要注意的一个地方。

2. for range 循环的时候追加元素,循环会否停止
v := []int{1, 2, 3}
for i := range v {
    v = append(v, i)
}

会的!for range 是语法糖,数组进行for range遍历前会对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  
 //   } 
3. 大数组遍历的问题
var arr = [204800]int{1, 1, 1} 
for i, n := range arr {     _ = i 
    _ = n 
}

在使用for range的时候会对原数组进行一次拷贝,但对大数组进行拷贝会十分浪费内存,如何解决?

  • 一种是取址遍历,for i, n := range &arr ,这时候拷贝的是地址,内存占用小很多
  • 另一种是对数组做切片引用 for i, n := range ``arr[:]

同时对于大型数组,由于参与for range的是该数组的拷贝,那么使用for range是不是会比经典for loop更耗资源且性能更差?结论是在编译器优化的情况下,性能甚至会更好,而在没有优化的情况下,两种loop的性能都大幅下降,并且for range下降更多,性能显著不如经典for loop。同时使用结构体方面:无论是哪种结构体类型,经典for loop遍历的性能都是一样的,但for range的遍历性能却会随着结构体字段数量的增多而下降。具体测试说明细节可以看看这篇文章:传送门

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 once sync.Once 
for i := range m {
    once.Do(func() {
        for _, key := range []int{1, 2, 3} {
            if key != i {
                fmt.Printf("当前 i:%d, del key:%d\n", i, key)
                delete(m, key)
                break
             }
         }
    })
    fmt.Printf("%d%d\n", i, m[i])
}

map删除的元素后面不会被访问到了,这里once的作用主要保证了map会有一个键值对被del,后面print只会print两个键值对。注意的是once里面的函数是一个for循环,会把1,2,3都取一次,直到取出来的和map随机取出来的不一样,也就是key != i

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

可能会。map的for range是随机化的,具有不确定性,一种是遍历顺序的不确定性,还有一种是遍历范围的不确定性。

6. for range里面使用go routine
var m = []int{1, 2, 3}
for i := range m {
    go func() {
        fmt.Print(i)
    }()
}
//阻塞1分钟等待所有goroutine运行完 time.Sleep(time.Millisecond)

以为的结果会是0,1,2;实际的结果是2,2,2。很有可能当for循环执行完之后,goroutine才开始执行,这个时候val的值是指向了切片中最后一个元素。可以通过参数方式传入或者使用局部变量拷贝。

for i := range m {
    go func(i int) {
        fmt.Print(i)
    }(i)
}
for i := range m {
    j := i
    go func() {
        fmt.Print(j)
    }()
}