golang-for range的坑

golang中,常用的遍历方式有经典的for循环和for range两种。实际上,使用 for range 语法的控制结构最终应该也会被golang的编译器转换成普通的 for 循环,所以for range实际上会被先转换成经典的for循环再真正执行,而正是这个转换过程常常会留坑。
下面简单表示这个转换过程:
for range代码是:

for index, value := range t_slice {
	original body
}

转换后:

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

实际编码中可能遇到的问题

循环会不会停?

代码如下:

package main

import "fmt"

func main() {
	arr := []int{1,2}

	for _, v := range arr {
		arr = append(arr, v)
	}

	fmt.Println(arr)

}

结果:

[1 2 1 2]
  • 1

从上面的转换过程可以看到,循环在还没开始时已经拿到了循环的长度,在循环的过程中不会改变。

遍历取所有元素地址?

代码如下:

package main

import "fmt"

func main() {
	arr := []int{1,2}
	res := []*int{}

	for _, v := range arr {
		res = append(res, &v)
	}

	fmt.Println(*res[0], *res[1])
}

结果:

2 2

由上面的转换过程可以看出,&v实际上是对循环内部同一个短变量的取址,因此res中存的其实都是同一个地址,这个地址中的值实际上是最后一次循环赋的值。
那若要取所有元素地址怎么做呢?
第一种方式:

package main

import "fmt"

func main() {
	arr := []int{1,2}
	res := []*int{}

	for _, v := range arr {
		v := v
		res = append(res, &v)
	}

	fmt.Println(*res[0], *res[1])
}

实际上是在original body中引入一个新短变量替换原value;
第二种方式:

package main

import "fmt"

func main() {
	arr := []int{1,2}
	res := []*int{}

	for i := range arr {
		res = append(res, &arr[i])
	}

	fmt.Println(*res[0], *res[1])
}

实际上就是for循环;
这两种方式得到的结果:

1 2

在遍历中起协程?

代码如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m = []int{1, 2, 3}
	var wg sync.WaitGroup
	for i := range m {
		wg.Add(1)
 		go func() {
			fmt.Print(i)
			wg.Done()
    	}()
	}
	wg.Wait()

}

输出结果:

222

上面的代码中,每轮循环中使用匿名函数起协程,匿名函数直接引用外部变量i,实际上,这就是golang中的闭包,闭包是匿名函数与匿名函数所引用环境的组合,匿名函数有动态创建的特性,这使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量。
每轮循环启动一个协程,而协程启动与循环变量递增不是在同一个协程,协程启动的速度远小于循环执行的速度,所以即使是第一个协程刚起启动时,循环变量可能已经递增完毕。由于所有的协程共享循环变量i,而且这个i会在最后一个使用它的协程结束后被销毁,所以最后的输出结果都是循环变量的末值即2。

package main

import (
	"fmt"
	"time"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	var m = []int{1, 2, 3}
	for i := range m {
		wg.Add(1)
 		go func() {
			fmt.Print(i)
			wg.Done()
		}()
		time.Sleep(time.Second)
	}
	wg.Wait()

}

这样写,使得循环变量递增时间间隔增大到1s,足够协程启动,此时输出结果:

012

怎样更科学的解决这个问题呢?实际上跟前面类似,有以下两种方式:
第一种:
以参数方式传入:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m = []int{1, 2, 3}
	var wg sync.WaitGroup
	for i := range m {
		wg.Add(1)
 		go func(i int) {
			fmt.Print(i)
			wg.Done()
    	}(i)
	}
	wg.Wait()

}

第二种:
使用局部变量拷贝:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m = []int{1, 2, 3}
	var wg sync.WaitGroup
	for i := range m {
		wg.Add(1)
		i := i
 		go func() {
			fmt.Print(i)
			wg.Done()
    	}()
	}
	wg.Wait()

}

注:

  1. 对于大数组,如果使用for range遍历,遍历前的转换过程会很浪费内存,可以优化:
    (1)对数组取地址遍历for i, n := range &arr;(2)对数组做切片引用for i, n := range arr[:];

  2. 对于大数组的遍历重置为默认值,golang底层有优化,因此效率很高;

  3. map遍历时删除元素,如果删除的元素开始没被遍历到,之后就不会出现;

  4. map遍历时新增元素,可能会被遍历到;

猜你喜欢

转载自blog.csdn.net/feikillyou/article/details/112535192