问题
闭包 是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
比如下面“斐波那契数列”闭包:
func fib() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
调用如下
f00 := fib()
fmt.Println(f00(), f00(), f00(), f00(), f00())
输出结果是:1 1 2 3 5
golang里是如何做到这种闭包管理的呢?
闭包实现
我们先对闭包分3种场景:
- 闭包里没有引用环境(变量生命周期很短,调用完即释放)
- 闭包里引用全局变量(变量生命周期就是全局变量生命周期)
- 闭包里引用局部变量(变量生命周期长,调用完不释放,下次调用会继续引用)
分别对3种场景以以下代码进行分析:
var y int
// 第一种场景
func fib01() func() int {
return func() int {
a, b := 0, 1
a, b = b, a+b
return a
}
}
// 第二种场景
func fib00() func() int {
return func() int {
y++
return y
}
}
// 第三种场景
func fib3(x int) func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
x++
return a+x
}
}
保存上述文件为closure.go,然后在用汇编工具生成汇编代码。汇编参考golang内核系列–深入理解plan9汇编&实践
git:git@github.com:buptbill220/gotls.git
使用如下命名生成汇编:
go tool compile -S closure.go > closure.S
我们打开closure.S找到对应的函数闭包位置
第一种场景fib01
然后找到 "".fib01.func1
闭包在全局区定义到代码段
"".fib01.func1·f SRODATA dupok size=8
我们可以看到:这种情况就是一种普通的函数,直接返回函数地址然后调用
为了帮助理解这个实现,我们手动使用汇编根据一个函数地址调用函数
TEXT ·CallTest(SB),NOSPLIT,$0-8
MOVQ arg+0(FP), AX
CALL AX
RET
func call_test() {
fmt.Printf("call test")
}
func CallTest(uintptr)
x := call_test
CallTest(**(*(*uintptr))(unsafe.Pointer(&x)))
结果输出:call test
第二种场景fib00
然后找到 “”.fib00.func1
y的位置
"".y SNOPTRBSS size=8
我们可以看到,第二种场景也是返回函数闭包的地址,只是闭包内部访问全局变量,并不做额外的工作。可以和场景1归纳为一类
第三种场景fib3
由于汇编较多,拆为2部分
然后找到 “”.fib3.func1
通过上述分析,我们可以得知,所有引用局部变量,Golang在生成汇编是帮我们在堆上创建该变量的一个拷贝,并把该变量地址和函数闭包组成一个结构体,并把该结构体传出来作为返回值。
结构体形式如下:
type FF struct {
F unitptr
B *int
A *int
X *int // 如果X是string/[]int,那么这里应该为*string,*[]int
}
这个结构有个特点:结构里变量顺序和函数里引用顺序刚好相反。这是因为Golang为了保持物理地址顺序一致性的结果。
- 栈的物理空间增长顺序是从大到小,栈里看到地址顺序是x>a>b
- 而我们引用的是堆上物理空间增长顺序是从小到大,为了保持和栈上物理地址顺序一致,生成的结构顺序就是b、a、x
为了帮助理解这种实现,我们手动把函数闭包转换成结构并输出:
type FF struct {
F unitptr
b *int
a *int
x *int
}
f := fib3(0)
ptr := *(**FF)(unsafe.Pointer(&f))
fmt.Printf("ptr %v, %d, %d, %d\n", ptr, *ptr.a, *ptr.b, *ptr.x)
fmt.Println(f(), f(), f(), f(), f())
fmt.Printf("ptr %v, %d, %d, %d\n", ptr, *ptr.a, *ptr.b, *ptr.x)
自己手动调试看看
这里还有另一个特点:函数闭包内部本身是通过寄存器来访问引用环境的变量,在闭包调用前会把该结构地址提前放置寄存器(这里放到DX)
我们可以看看闭包调用前的代码:
总结
golang的函数闭包实现主要分为2中场景:
- 闭包里没有引用环境&获取引用全局变量。这种场景下,其实现就是普通的函数,按照普通的函数调用方式执行闭包调用。
- 闭包里引用局部变量。这种场景下,才是真正的闭包(函数+引用环境),并且以一个struct{FuncAddr, LocalAddr3, LocalAddr2, LocalAddr1}结构存储该闭包,等到调用闭包时,会把该结构地址提前放置一个寄存器,闭包内部通过该寄存器访问引用环境的变量
有疑问加站长微信联系(非本文作者)