Jquery中文网 www.jquerycn.cn
Jquery中文网 >  后端编程  >  Go语言  >  正文 golang-for range的坑

golang-for range的坑

发布时间:2021-05-10   编辑:www.jquerycn.cn
jquery中文网为您提供golang-for range的坑等资源,欢迎您收藏本站,我们将为您提供最新的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]

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

遍历取所有元素地址?

代码如下:

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遍历时新增元素,可能会被遍历到;

到此这篇关于“golang-for range的坑”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Go 编程 range 有点坑
golang append
Golang面试题总结
Golang基础--常见坑
go golang 笔试题 面试题 笔试 面试
golang 获取 进程 名称 id
golang-for range的坑
golang key map 所有_golang之map
golang 按key字母顺序遍历map
golang slice map 的一些常见坑

[关闭]