前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang中的for range语义的理解

golang中的for range语义的理解

原创
作者头像
刘斌
修改2019-08-04 12:37:08
3.1K0
修改2019-08-04 12:37:08
举报
文章被收录于专栏:高性能网络编程

golang中for range经常会被用来遍历slice、map、chan、array,但是由于在某些情况下,其内部实现并不是你想的那样,所以使用时还是需要特别注意。

以下是两个错误使用for range的场景

场景1 - 在for range中获取循环变量的地址

代码

代码语言:txt
复制
package main

func main() {
	vals := []int{0, 1, 2}
	valptr := make([]*int, 0)

	for k, v := range vals {
        //v := v
		valptr = append(valptr, &v)
	}

	for _, v := range valptr {
		println(v)
		println(*v)
	}
}

输出

代码语言:txt
复制
0xc000016140
6
0xc000016140
6
0xc000016140
6

分析

The iteration variables in forstatements are reused in each iteration. This means that each closure (aka function literal) created in your for loop will reference the same variable (and they'll get that variable's value at the time those goroutines start executing).

go runtime中for range循环只会为v分配一次内存,后续只是给v赋值;跟for的语义是一样一样的,如下这样理解起来就容易多了。

代码语言:txt
复制
package main

func main() {
	for i := 0; i < 3; i++ {
		println("&i=", &i, " i=", i)
	}
}

场景2 - 在closure中捕获循环变量

代码

代码语言:txt
复制
package main

func main() {
	kvs := map[string]int{
		"zero": 0,
		"one":  1,
		"two":  2,
	}

	for _, v := range kvs {
		//v := v
		defer func() {
			println("defer func(): ", v)
		}()
	}
}

输出

代码语言:txt
复制
# go run main.go 
go func():  2
go func():  2
go func():  2
# go vet
# github.com/username/gomodule
./main.go:19:27: loop variable v captured by func literal

分析

closure(func literal)中捕获变量是通过引用的方式,因此也会像场景1中那样,v的地址虽然没变,但是值会随着循环变化。

同时,在场景2中,go vet会有对应的提示:

./main.go:19:27: loop variable v captured by func literal

在出现以上提示时,需要特别小心。同时这里要说下:

go vet是一个很有价值的工具,可以帮助我们发现很多代码问题

但是场景1是没有的提示, 也不能完全依赖go vet来发现错误。

除了通过增加v := v的方式,还可以通过以下方式:

代码语言:txt
复制
package main

func main() {
	kvs := map[string]int{
		"zero": 0,
		"one":  1,
		"two":  2,
	}

	for _, v := range kvs {
		defer func(v int) {
			println("defer func(): ", v)
		}(v)
	}
}

其实原理是一样的,

  • v (v1) := v (v2) 括号是注释是显式的创建了一个v的副本,也叫v; 这里两个v的生命周期不同, v2的生命周期是整个for循环,v1的生命周期是for循环中的一个循环,但是这里由于closure对于v1的引用,所以在一个循环结束后,v会发生逃逸,并不会被立即回收;
  • 把v作为closure的参数,通过golang的pass-by-value,隐式的创建了一个v的副本;

大部分刚入门golang的开发者都会犯类似错误,讲道理这个可以算是语言的缺陷了,毕竟让用户少犯错也是语言的义务。因此社区中会很多proposal想要解决这个问题。

Proposal

proposal: spec: redefine range loop variables in each iteration

已经有人提了类似Proposal, 有可能Go 2.0中会彻底解决这个问题。

如下:

代码语言:txt
复制
for k, v := range vals {
  // ...
}

should be equivalent to

代码语言:txt
复制
for k, v := range vals {
  k := k
  v := v
  // ...
}

参考

https://www.ardanlabs.com/blog/2013/09/iterating-over-slices-in-go.html

https://tam7t.com/golang-range-and-pointers/

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景1 - 在for range中获取循环变量的地址
    • 代码
      • 输出
        • 分析
        • 场景2 - 在closure中捕获循环变量
          • 代码
            • 输出
              • 分析
              • Proposal
              • 参考
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档