什么是闭包
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。
引用自维基百科
这段描述里面有几个关键点:
- 闭包是一个结构体,里面存储了一个函数和一个关联的环境
- 环境里包括函数内部的局部变量,也包括函数外部定义但在函数内部引用的自由变量
- 对于值的处理可以是值拷贝,也可以是引用
闭包问题
- 延迟绑定
- 内存逃逸
什么是延迟绑定?
先给出三个例子,后面会解释输出这些结果的原因,请耐心看完
例子一:
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,为什么延时一秒输出就正常了
为什么以上例子会出现这些结果
- 延时绑定
- 闭包定义的时候并不是真正的在执行,只有当我们调用的时候才真正的执行,如例子一。每次执行的时候他都会去找到他引用环境的最新值
- 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 变量。
揭秘
例子一:
- 定义闭包 - 这个时候闭包只是定义,并没有使用外部的值
- 执行,执行的时候闭包回去寻找最新的环境引用值,有以上go for 循环底层实现分析可知,我们实际上使用的是值 v2,v2每次都会重新覆盖。所以当闭包引用环境最新的值时,实际访问的都是v2,所以最终访问到的 val 都是5
例子二:
- 定义闭包
- 异步执行闭包
- 异步执行的过程寻找最新的 v2,由于是并发执行的,所以多个协程很可能访问到同一个 v2
例子三:
- 执行过程和例子二一致,但是为什么加一秒延时就正常了,延时主要是导致协程不在并发执行了,所以每个协程获取到的都是当前引用环境的 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
Github:wang1309