闭包

Function Value

go中函数是头等对象,可以作为参数传递,可以作为返回值,也可以绑定到变量。go中称这样的参数,返回值或者变量为Function Value。

Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体,这个结构体里只有一个地址,就是这个函数指令的入口地址

image-20221220230702063

image-20221220230844375

函数A被赋值给f1和f2两个变量,这种情况,编译器会做出优化,让f1和f2共用一个funcval结构体。

如果函数A的指令在这,入口地址addr1,编译阶段,会在只读数据段分配一个funcval结构体,fn指向函数A指令入口,而它本身的起始地址,会在执行阶段赋给f1和f2。通过f1来执行函数,就会通过它存储的地址找到对应的funcval结构体,拿到函数入口地址,然后调转执行。

既然只要有函数入口地址就能调用 ,为什么要通过funcval结构体包装这个地址,然后使用一个二级指针来调用呢?这里主要是为了处理闭包的情况。

闭包

  • 第一:必须要有在函数外部定义,但在函数内部引用的自由变量
  • 第二:脱离了形成闭包的上下文,闭包也能照常使用这些自由变量。
1
2
3
4
5
6
7
8
9
10
11
12
13

func create() func(){
c := 2
return func(){
fmt.Println(c)
}
}
func main(){
f1 := create()
f2 := create()
f1()
f2()
}

create函数的返回值是一个函数,并且引用了其外层函数定义的局部变量c;而且,即便create函数结束,依然可以通过f1和f2正常执行这个函数并使用定义在create内部的变量c。所以这个返回值符合闭包的定义,而这个自由变量c,通常被称为“\捕获变量**”。

image-20221220231610359

在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已

每个闭包对象都是一个Function Value,但是各自持有自己的捕获列表,这也是称闭包为有状态的函数的原因。

捕获列表

被闭包捕获的变量,要在外层函数与闭包函数中表现一致,好像它们在使用同一个变量,为此,go语言的编译器针对不同情况做了不同的处理。

被捕获的变量没有被修改的情况

最简单的情况就像上面那个例子,被捕获的变量除了初始化赋值外,在任何地方都没有被修改过,所以直接拷贝值到捕获列表就ok了

被捕获的变量被修改的情况

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

func create() (fs [2]func()){
for i := 0; i < 2; i++ {
fs[i] = func(){
fmt.Println(i)
}
}
return
}
func main() {
fs := create()
for i := 0; i < len(fs); i++ {
fs[i]()
}
}

在这个例子中,被捕获的是局部变量i,而且除了初始化赋值(i:=0)外还被修改(i++)过.

image-20221220232033063

闭包函数指令入口addrf,main函数栈帧中,局部变量fs是一个长度为2的function value类型数组,返回值为零值,到create函数栈帧,由于被闭包捕获并修改,局部变量i改为堆分配,在栈上只存一个地址

第一次for循环。在堆上创建funcval结构体,捕获i的地址,这样闭包函数就和外层函数操作同一个变量了,返回值第一个元素存储addr0,第一次for循环结束,i自增1

第二次for循环开始,再次堆分配一个funcval,捕获变量i的地址,第二个元素存储addr1,第二次循环结束,i再次自增1,create函数结束,把返回值拷贝到局部变量fs。

通过fs[0]调用函数时,把addr0存入寄存器。闭包函数通过寄存器存储的地址加上偏移找到捕获变量i的地址。fs[1]同理,被捕获的地址都指向它,所以每次都会打印2。

闭包导致的局部变量堆分配,也是变量逃逸的一种场景。

image-20221220232513995

有修改并被捕获的是参数

如果有修改并被捕获的是参数,涉及到函数原型,就不能像局部变量那样处理了。

参数依然通过调用者栈帧传入,但是编译器会把栈上这个参数拷贝到堆上一份。然后外层函数和闭包函数都使用堆上分配的这一个。

image-20221220232609842

被捕获的是返回值

如果被捕获的是返回值,处理方式又有些不同

调用者栈帧上依然会分配返回值的空间,不过闭包的外层函数会在堆上也分配一个,外层函数和闭包函数都使用堆上这个,但是在外层函数返回前,需要把堆上的返回值拷贝到栈上的返回值空间

image-20221220232752505

总结

处理方式虽然多样,但是目标只有一个,就是保存捕获变量在外层函数和闭包函数中的一致性.

参考链接

幼麟实验室bilibili:https://space.bilibili.com/567195437/?spm_id_from=333.999.0.0