一次讲清 go 闭包及问题

什么是闭包

​ 在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

引用自维基百科

这段描述里面有几个关键点:

  1. 闭包是一个结构体,里面存储了一个函数和一个关联的环境
  2. 环境里包括函数内部的局部变量,也包括函数外部定义但在函数内部引用的自由变量
  3. 对于值的处理可以是值拷贝,也可以是引用
闭包问题
  1. 延迟绑定
  2. 内存逃逸
什么是延迟绑定?

先给出三个例子,后面会解释输出这些结果的原因,请耐心看完

​ 例子一:

func foo(x int) []func() {
    var fs []func()
    values := []int{1, 2, 3, 5}
    for _, val := range values {
        fs = append(fs, func() {
            fmt.Printf("foo7 val = %d\n", x+val)
        })
    }
    return fs
}
// Q4实验:

for _, f7 := range foo(11) {
    f7()
}

输出:

foo7 val = 16
foo7 val = 16
foo7 val = 16
foo7 val = 16

结果输出的都是16,我们期望输出的是12,13,14,16 。这个坑我们在工作中也会经常碰到

​ 例子二:

for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}

	time.Sleep(1 * time.Second)

我电脑上的输出结果:5 10 10 10 10 10 10 10 10 10,实际上每次执行输出结果都不一样

例子三:

for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}

	time.Sleep(1 * time.Second)

输出结果:1 2 3 4 5 6 7 8 9,为什么延时一秒输出就正常了

为什么以上例子会出现这些结果
  1. 延时绑定
    1. 闭包定义的时候并不是真正的在执行,只有当我们调用的时候才真正的执行,如例子一。每次执行的时候他都会去找到他引用环境的最新值
  2. Go 语言遍历数组和切片时会复用变量

结合这两点就可以很好的解释以上三个例子的现象了,可能这样说还不是很明白,接下来详细介绍一下第二点

Go 语言遍历数组和切片时会复用变量

当我们执行下面这断代码时

for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}

go 语言底层的实现实际上是

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

看出来了吗,go 底层每次都会把值赋给 temp, 然后把 temp 赋值给 v2,实际上我们访问的是 v2 变量。

揭秘

例子一:

  1. 定义闭包 - 这个时候闭包只是定义,并没有使用外部的值
  2. 执行,执行的时候闭包回去寻找最新的环境引用值,有以上go for 循环底层实现分析可知,我们实际上使用的是值 v2,v2每次都会重新覆盖。所以当闭包引用环境最新的值时,实际访问的都是v2,所以最终访问到的 val 都是5

例子二:

  1. 定义闭包
  2. 异步执行闭包
    1. 异步执行的过程寻找最新的 v2,由于是并发执行的,所以多个协程很可能访问到同一个 v2

例子三:

  1. 执行过程和例子二一致,但是为什么加一秒延时就正常了,延时主要是导致协程不在并发执行了,所以每个协程获取到的都是当前引用环境的 v2
如何解决延时绑定带来的问题
值覆盖
for i := 0; i < 10; i++ {
		i := i
		go func() {
			fmt.Println(i)
		}()
	}

这样每个协程访问的就不是变量 v2了,而是每次寻找自己引用环境里面的 i 值,所以都能正确输出,在很多开源项目都能见到这种写法,比如 kratos

值传递
for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}

	time.Sleep(1 * time.Second)

把当前值直接传递到闭包里面,自然输出的就是当前引用环境自身的值

这次主要是分析延时绑定的问题,关于闭包的内存逃逸大家可以看一下极客兔兔的文章,参看下面的链接。

Reference
  1. 维基百科 - 闭包
  2. go 设计与实现 - for range
  3. 极客兔兔 - 内存逃逸
  4. 知乎 - go 闭包

Github:wang1309

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值