Golang 循环体中的闭包和go func变量取值问题[延迟绑定]


其实不知道取这个标题表达的准不准确,有些文章会说是延迟绑定问题

闭包定义

闭包是由函数和与其相关的引用环境组合而成的实体 。
函数,指的是在闭包实际实现的时候,往往通过调用一个外部函数返回其内部函数来实现的。内部函数可能是内部实名函数、匿名函数或者一段lambda表达式。用户得到一个闭包,也等同于得到了这个内部函数,每次执行这个闭包就等同于执行内部函数
环境,具体地说,在实际中引用环境是指外部函数的环境,闭包保存/记录了它产生时的外部函数的所有环境。

举例说明

func incr() func() int {
    var x int
    return func() int {
        x++
        return x
    }
}

调用这个函数会返回一个函数类型【func】的变量
i := incr():通过把这个函数变量赋值给 i,i 就成为了一个闭包
所以 i 保存着对 x 的引用,可以想象 i 中有着一个指针指向 x 或 i 中有 x 的地址。
由于 i 有着指向 x 的指针,所以可以修改 x,且保持着状态

println(i()) // 1
println(i()) // 2
println(i()) // 3

但是这段代码却不会递增:

println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1

这是因为这里调用了三次 incr(),返回了三个闭包,这三个闭包引用着三个不同的 x,它们的状态是各自独立的。

循环体中闭包变量的延迟绑定问题

个人认为延迟绑定这个描述不太合适,因为其实是个解引用的过程

举例说明

func t1(x int) []func() {
	var fs []func()
	values := []int{1, 2, 3, 5}
	for _, val := range values {
		fmt.Printf("outer val = %d, addr = %v\n", x+val, &val)
		fs = append(fs, func() {
		//这里传入的实际上是val的引用
			fmt.Printf("inner val = %d, addr = %v\n", x+val, &val)
		})//这里只是声明,不会立刻执行,调用内部函数时才会真正执行
	}
	return fs
}
func main(){
	funcs := t1(11)
	fmt.Println("declare done")
	for _, f := range funcs {
		f()//这里才会执行
	}
}
//out:
outer val = 12, addr = 0xc0000ac058
outer val = 13, addr = 0xc0000ac058
outer val = 14, addr = 0xc0000ac058
outer val = 16, addr = 0xc0000ac058
declare done
inner val = 16, addr = 0xc0000ac058
inner val = 16, addr = 0xc0000ac058
inner val = 16, addr = 0xc0000ac058
inner val = 16, addr = 0xc0000ac058

解释:闭包保存/记录了它产生时的外部函数的所有环境。如同普通变量/函数的定义和实际赋值/调用或者说执行,是两个阶段。闭包也是一样,for-loop内部仅仅是声明了一个闭包,t1()返回的也仅仅是一段闭包的函数定义,只有在外部执行了f()时才真正执行了闭包,在执行这个闭包的时候,会去其外部环境解引用,说这四个函数引用的都是同一个变量val的地址,所以之后val改变,解引用得到的值也会改变,所以这四个函数都会输出最后的最新的值。
插播一下: 在for range里面val的地址为什么始终没有改变,也就是始终是同一个变量,可以看一下这里的文章:Go语言设计与实现

ha := a  //a是原切片
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

简单来说就是,golang的for range机制相当于对for循环做了优化,会额外创建一个新的 v2 变量存储切片中的元素,循环中使用的这个变量 v2的 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝,而其本身地址不会变,因为始终是同一变量;也就引出了另一个需要注意的for range用法中的指针问题:【获取 range 返回变量的地址并保存到另一个数组】

func main() {
	arr := []int{1, 2, 3}
    newArr := []*int{}
    for _, v := range arr {
        newArr = append(newArr, &v)
        fmt.Printf("addr:%v\n", &v)
    }
    for _, v := range newArr {
        fmt.Println(*v)
    }
}
    //out:
addr:0xc0000a6010
addr:0xc0000a6010
addr:0xc0000a6010
3
3
3
//正确写法
func main() {
	arr := []int{1, 2, 3}
	newArr := []*int{}
	for i, _ := range arr {
		newArr = append(newArr, &arr[i])
	}
	for _, v := range newArr {
		fmt.Println(*v)
	}
}
//out
1
2
3

再举个栗子加深一下印象

func t2() func() {
	x := 1
	f := func() {
		fmt.Printf("t2 val = %d\n", x)
	}
	x = 11
	fmt.Println("declare done")
	return f//这里返回这个函数之后才会执行该内部函数
}
func main(){
	t2()()//第二个括号代表执行返回的内部函数
}
//out
declare done
t2 val = 11

goroutine延迟绑定-在循环迭代器的变量上使用 goroutines

举例说明

func show3(v interface{}) {
	fmt.Printf("t3 val = %v\n", v)
}
func t3() {
	values := []int{1, 2, 3, 5}
	for _, val := range values {
		go show3(val) //正常打印,等效于:
		//      go func(val interface{}) {
		//		fmt.Println(val)
		//   	}(val)
		//将 val 作为参数添加到闭包中,val在每次迭代时评估并放置在 goroutine 的堆栈中
	}
	fmt.Println("t3 declare done")
}

type myType int

func (v *myType) show4() {
	fmt.Printf("t4 val = %v\n", *v)
}
func t4() {
	values := []myType{1, 2, 3, 5}
	for _, val := range values {
		//解决方法:newVal := val
		go val.show4() //t5原理相同,绑定的是同一个val,但值不确定,因为
		//for循环是运行在主协程中的,负责改变val值,当前子协程运行时val为多少就输出多少,
		//由于for循环很快就跑完了,而子协程可能还没被调度。所以一般会输出最后的val值
	}
	fmt.Println("t4 declare done")
}

func t5() {
	values := []int{1, 2, 3, 5}
	for _, val := range values {
		go func() {//这个匿名函数就是一个闭包
			fmt.Printf("t5 val = %v\n", val)//和t4原理相同
		}()//当程序执行到这里时,只是声明,还没被调度
		//time.Sleep(time.Duration(1) * time.Millisecond)
		//主协程如果能等等,这个子协程被调度执行后再继续就可以了,但肯定不能这么搞,太慢了
	}
	fmt.Println("t5 declare done")
}//val的生命周期在闭包内部被延长了
func main(){
	t3()
	t4()
	t5()
	//hold住,等待其他协程执行完
	time.Sleep(time.Second)
}

输出结果
在这里插入图片描述
上面的t4和t5的注释写的很清楚,出现打印的全是最后一个值也就是5的情况是因为主协程【暂且这么叫,或者叫第一个协程,并协程之间是平等的,没有主次之分】和子协程是可以一起操作val这个变量的,但很明显主协程先被调度,for循环也跑得很快,以至于子协程还没被调度,for循环就已经跑完了,val的值已经变成了5…

良好实践

至于t3,就是一个良好的实践,再举一个栗子说明这种实践:将内部函数所用到的变量作为外部函数的入参传进去,这样相当于每轮循环中形成的闭包中的i都是不同的i

//修改前
func main(){
	var funcSlice []func()
	for i := 0; i < 3; i++ {
		funcSlice = append(funcSlice, func() {
			fmt.Printf("addr: %v,value %v \n", &i, i)
		})
	}
	for j := 0; j < 3; j++ {
		funcSlice[j]() 
	}
}
//out
addr: 0xc00000a088,value 3 
addr: 0xc00000a088,value 3 
addr: 0xc00000a088,value 3 
//修改后
func main(){
	var funcSlice []func()
	for i := 0; i < 3; i++ {
		func(i int) {
			funcSlice = append(funcSlice, func() {
				fmt.Printf("addr: %v,value %v \n", &i, i)
			})
		}(i)
	}
	for j := 0; j < 3; j++ {
		funcSlice[j]() // 0, 1, 2
	}
}
//out
addr: 0xc00000a088,value 0 
addr: 0xc00000a0a0,value 1 
addr: 0xc00000a0a8,value 2 

或者像t4的注释中写的那种解决方法,每轮循环都新建一个变量传进去

我再举个栗子

func main(){
   for i := 0; i < 10; i++ {
      go func() {
         fmt.Println(i)
      }()
   }
   time.Sleep(time.Second)//hold住主协程【第一个协程】,等待剩下的协程执行完
}

输出:下面的2就是启动的比较快的一个子协程 ,此时主协程的for循环i才跑到2;
在这里插入图片描述
另外值得注意的是,此时i的最新值为10而不是9,也就是说,注意! 上面的t1如果改成这样遍历切片会报错:

func t1(x int) []func() {
	var fs []func()
	values := []int{1, 2, 3, 5}
	for i := 0; i < len(values); i++{
		fs = append(fs, func() {
			fmt.Printf("val = %d\n", x+values[i])
		})
	}
	return fs
}
func main(){
	funcs := t1(11)
	fmt.Println("declare done")
	for _, f := range funcs {
		f()//这里才会执行
	}
}

在这里插入图片描述

测试一下go func从声明到被调度执行需要的时间

func t6() {
	for i := 1; i < 10; i++ {
		curTime := time.Now().UnixNano()
		go func(t1 int64) {
			t2 := time.Now().UnixNano()
			fmt.Printf("t6 ts = %d us \n", t2-t1)
		}(curTime)
	}
}
func main() {
	go t6()
}

我电脑好像有点拉垮了
在这里插入图片描述

再插播一下

在测试t4的时候顺便看了下地址,复习了一下指针

func (v *myType) show4() {
	fmt.Printf("t4 pointer addr: %v addr in pointer: %v & value in pointer: %v \n", &v,v,*v)
}

func t4() {
	values := []myType{1, 2, 3, 5}
	for _, val := range values {
		fmt.Printf("t4 for loop addr & value:%v %v\n", &val, val)
		go val.show4()
	}
}

在这里插入图片描述
参考文章:
https://zhuanlan.zhihu.com/p/92634505
https://segmentfault.com/a/1190000022798222

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值