01
—
函数
函数,在GO语言中属于头等对象,可以被当作参数传递、也可以作为函数返回值、绑定到变量。Go语言称这样的参数、返回值和变量为“Function Value”。
Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体。
type funcval struct {fn uintptr}

图:一个Function Value的存在形式
02
—
闭包
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。 维基百科
所以像下面这个例子:
func create() func(){c := 2return func(){fmt.Println(c)}}func main(){f1 := create()f2 := create()f1()f2()}
虽然create函数的返回值函数形成闭包,但是Go语言里并没有把闭包从Function Value中特别区分出来。在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已。这些捕获变量就是它的捕获列表,就放在对应的funcval结构体的后面。所以上例中,f1和f2的内存布局如下图所示。

03
—
调用
继续使用闭包的示例,通过f1调用闭包函数时,会把f1存储的funcval结构体地址存入寄存器DX,这样在闭包函数的指令中就可以通过这个寄存器存储的地址加上8字节的偏移,就找到f1的捕获变量了。

同样的,通过f2调用闭包函数时,会把f2存储的funcval结构体地址存入寄存器,闭包函数执行时找到的就是f2的捕获变量了。

如果是没有捕获列表的Function Value,直接忽略这个寄存器即可。通过这样的方式,Go语言实现了对Function Value的统一调用。对Function Value有了大致了解,就可以关注一些细节了。
04
—
静态分配
func A(i int) {i++fmt.Println(i)}func B(){f1 := Af1(1)}func C(){f2 := Af2(1)}
像上面这种情况,编译阶段会创建一个funcval结构体放到只读数据段,而执行阶段,f1和f2都会使用它。

05
—
捕获列表
因为捕获列表需要由闭包对象各自持有,所以有捕获列表的Function Value要到执行阶段才会在堆上分配对应的funcval结构体以及捕获列表空间。但是,捕获列表里存什么?直接拷贝捕获变量值吗?才没有那么简单。
闭包捕获的变量要在闭包函数和外层函数中表现一致,如果单纯值拷贝,就无法保证这一点。所以编译器针对捕获变量的不同情况分别做出了不同的处理。
捕获变量除了初始化赋值外在任何地方都没有被修改过,那就可以直接拷贝值,因为它不会再变化。
捕获变量除了初始化赋值外,还被修改过,就要再细分了。
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改为堆分配,栈上只存一个地址。我们把闭包函数指令入口地址记为addrf。

main函数栈帧中,局部变量fs,以及create函数的返回值都是长度为2的Function Value型数组。在create函数栈帧中,局部变量i被闭包捕获,分配到了堆上,在栈上的局部变量空间只存储它在堆上的地址。在for循环执行以前,i等于0。

在堆上创建一个funcval结构体,指向闭包函数入口,并且在捕获列表中存储捕获变量i的地址,这样就可以和外层函数使用同一个变量了。返回值数组第一个元素存储这一次创建的funcval结构体地址,第一次for循环结束后,i自增1。

第二次for循环再次创建一个funcval结构体,存储捕获变量i的地址,而funcval结构体本身的地址作为返回值的第二个元素。第二次循环结束,i再次自增1。
如果是参数被捕获,那么调用者依然从栈上传递参数,但是被调用函数会把它拷贝到堆上一份,然后和闭包函数都使用堆上分配的那一个。

如果是返回值被捕获,那么处理方式就又有些不同了。返回值空间依然由调用者在栈上分配,但是被调用函数(闭包的外层函数)会在堆上也分配一个,并且与闭包函数都使用堆上这一个。但是,在外层函数返回前要把堆上的返回值拷贝到栈上那一个。

06
—
关键点
(1)Go语言里Function Value本质上是指向funcval结构体的指针;
(2)Go语言里闭包只是拥有捕获列表的Function Value;
(3)捕获变量在外层函数与闭包函数中要保持一致。

文章转载自幼麟实验室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




