基本概念
Go 语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。闭包也可以说是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。如下所示就是闭包的示意图:

前面的概念可能还是不太好理解,下面是关于闭包的汇编分析。
汇编分析
例子 1
使用闭包写一个非常简单的测试代码:
func main() { a := 1 func() int { a = 3 return a }()}使用go tool compile -S -N -l main.go命令生成的核心汇编代码如下:
main.main STEXT size=56 args=0x0 locals=0x28 funcid=0x0 align=0x0 0x0000 00000 (main.go:3) TEXT main.main(SB), ABIInternal, $40-0 // 栈溢出检查 0x0000 00000 (main.go:3) CMPQ SP, 16(R14) 0x0004 00004 (main.go:3) PCDATA $0, $-2 0x0004 00004 (main.go:3) JLS 49 0x0006 00006 (main.go:3) PCDATA $0, $-1 // 栈内存分配、保存旧栈的基地址和建立新栈帧 0x0006 00006 (main.go:3) SUBQ $40, SP 0x000a 00010 (main.go:3) MOVQ BP, 32(SP) 0x000f 00015 (main.go:3) LEAQ 32(SP), BP // gc垃圾处理 0x0014 00020 (main.go:3) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB) 0x0014 00020 (main.go:3) FUNCDATA $1, gclocals·EaPwxsZ75yY1hHMVZLmk6g==(SB) // a=1 0x0014 00020 (main.go:4) MOVQ $1, main.a+8(SP) // 把变量a的地址存储到AX寄存器 0x001d 00029 (main.go:8) LEAQ main.a+8(SP), AX 0x0022 00034 (main.go:8) PCDATA $1, $0 // 调用匿名函数 0x0022 00034 (main.go:8) CALL main.main.func1(SB) // BP地址恢复和栈空间回收 0x0027 00039 (main.go:9) MOVQ 32(SP), BP 0x002c 00044 (main.go:9) ADDQ $40, SP // 函数返回 0x0030 00048 (main.go:9) RET 0x0031 00049 (main.go:9) NOP 0x0031 00049 (main.go:3) PCDATA $1, $-1 0x0031 00049 (main.go:3) PCDATA $0, $-2 0x0031 00049 (main.go:3) CALL runtime.morestack_noctxt(SB) 0x0036 00054 (main.go:3) PCDATA $0, $-1 0x0036 00054 (main.go:3) JMP 0main.main.func1 STEXT nosplit size=61 args=0x8 locals=0x10 funcid=0x0 align=0x0 0x0000 00000 (main.go:5) TEXT main.main.func1(SB), NOSPLIT|ABIInternal, $16-8 // 栈内存分配、保存旧栈的基地址和建立新栈帧 0x0000 00000 (main.go:5) SUBQ $16, SP 0x0004 00004 (main.go:5) MOVQ BP, 8(SP) 0x0009 00009 (main.go:5) LEAQ 8(SP), BP // gc垃圾处理 0x000e 00014 (main.go:5) FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB) 0x000e 00014 (main.go:5) FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB) 0x000e 00014 (main.go:5) FUNCDATA $5, main.main.func1.arginfo1(SB) // 把AX寄存器的值(main函数a变量的地址)存储到栈上的某个位置---24(SP) 0x000e 00014 (main.go:5) MOVQ AX, main.&a+24(SP) // 初始化匿名函数的返回值 0x0013 00019 (main.go:5) MOVQ $0, main.~r0(SP) // 把a变量的地址存储到CX寄存器 0x001b 00027 (main.go:6) MOVQ main.&a+24(SP), CX // 将立即数3拷贝到CX寄存器指向的内存地址(a变量的内存地址) 0x0020 00032 (main.go:6) MOVQ $3, (CX) // 把a变量的地址存储到CX寄存器 0x0027 00039 (main.go:7) MOVQ main.&a+24(SP), CX // 把CX寄存器指向的内存地址中存储的值(变量a的内存地址)拷贝到AX寄存器 0x002c 00044 (main.go:7) MOVQ (CX), AX // 将AX寄存器中的值存储到函数返回值的位置 0x002f 00047 (main.go:7) MOVQ AX, main.~r0(SP) // BP地址恢复和栈空间回收 0x0033 00051 (main.go:7) MOVQ 8(SP), BP 0x0038 00056 (main.go:7) ADDQ $16, SP // 函数返回 0x003c 00060 (main.go:7) RET从汇编中可以看出,匿名函数可以实现对变量 a 的获取和修改是因为在调用匿名函数之前,main 函数将变量 a 的地址保存到了 AX 寄存器中。也就是说,现在 main 函数的栈中除了保存了变量 a 的值,还有对于寄存器 AX 的引用。那么对于匿名函数,只需要获取寄存器 AX 的值就能知道变量 a 的地址,进而可以对变量 a 进行修改。
所以,匿名函数本质上是通过寄存器 AX 来捕捉到 main 函数栈中的变量 a,那么这种引用(通过 AX 寄存器来获得变量 a)和匿名函数的堆栈内存空间就构成了一个闭包。
在这个例子 1 之前,我尝试过在匿名函数内部只是获取变量 a,但不修改变量 a,结果就是 AX 寄存器会直接存储变量 a 的值,而不是地址。
例子 2
测试代码如下:
func test(x int) func() int { return func() int { x = 2 return x }}func main() { f := test(1) f()}使用go tool compile -S -N -l main.go命令生成的核心汇编代码如下(由于篇幅限制,在此省略掉部分汇编代码,比如栈空间操作、gc 垃圾处理和函数返回):
main.main STEXT size=60 args=0x0 locals=0x18 funcid=0x0 align=0x0 0x0000 00000 (main.go:10) TEXT main.main(SB), ABIInternal, $24-0 ······ // 传入参数1 0x0014 00020 (main.go:11) MOVL $1, AX 0x0019 00025 (main.go:11) PCDATA $1, $0 // 调用test()函数,该函数返回的是闭包对象的地址并保存到AX寄存器 0x0019 00025 (main.go:11) CALL main.test(SB) // 将闭包对象的地址保存到栈上 0x001e 00030 (main.go:11) MOVQ AX, main.f+8(SP) // 将闭包对象的地址中里面的数据加载到CX寄存器 // 这个数据是一个函数指针,该指针指向与闭包关联的函数 0x0023 00035 (main.go:12) MOVQ (AX), CX // 将闭包对象的地址保存到DX寄存器 0x0026 00038 (main.go:12) MOVQ AX, DX // 调用与闭包关联的函数 0x0029 00041 (main.go:12) CALL CX ······main.test STEXT size=175 args=0x8 locals=0x30 funcid=0x0 align=0x0 0x0000 00000 (main.go:3) TEXT main.test(SB), ABIInternal, $48-8 ······ // 参数传递,得到x为1 0x0018 00024 (main.go:3) MOVQ AX, main.x+56(SP) // 返回值初始化 0x001d 00029 (main.go:3) MOVQ $0, main.~r0+16(SP) // 获取type.int的地址,作为runtime.newobject()的参数 0x0026 00038 (main.go:3) LEAQ type.int(SB), AX 0x002d 00045 (main.go:3) PCDATA $1, $0 // 调用runtime.newobject()表示要在堆上分配内存,分配一个int类型的对象 // 调用完成后返回这块内存的地址保存在AX寄存器中 0x002d 00045 (main.go:3) CALL runtime.newobject(SB) // 将AX寄存器中的值复制到栈指针SP加32字节的位置 // 前面的main.&x表示这个内存位置的值(AX寄存器中的值)将作为变量x的地址 0x0032 00050 (main.go:3) MOVQ AX, main.&x+32(SP) // 把变量x的值(相对于栈指针SP加56字节的位置)加载到寄存器CX中 0x0037 00055 (main.go:3) MOVQ main.x+56(SP), CX // 把CX寄存器中的值(变量x的值)复制到AX寄存器指向的内存地址 // 通过上面的CALL指令和这三条MOVQ指令实现了:将x变量从栈区拷贝到了堆区(内存逃逸) 0x003c 00060 (main.go:3) MOVQ CX, (AX) // 获取闭包结构体的地址 0x003f 00063 (main.go:4) LEAQ type.noalg.struct { F uintptr; main.x *int }(SB), AX 0x0046 00070 (main.go:4) PCDATA $1, $1 // 调用runtime.newobject为闭包本身分配内存,分配的内存地址保存到AX寄存器中 0x0046 00070 (main.go:4) CALL runtime.newobject(SB) // 将AX寄存器中闭包对象的地址复制到栈上SP+24字节的内存位置 0x004b 00075 (main.go:4) MOVQ AX, main..autotmp_2+24(SP) // 将匿名函数的地址复制到CX寄存器 0x0050 00080 (main.go:4) LEAQ main.test.func1(SB), CX // 将匿名函数地址存入AX寄存器指向的堆地址(设置了闭包中的函数指针) 0x0057 00087 (main.go:4) MOVQ CX, (AX) // .autotmp_2通常表示一个自动分配的临时变量,这个变量其实是用来存储闭包对象的地址 // 将这个变量的值(在SP栈指针向上偏移24字节的位置)复制到DI寄存器 0x005a 00090 (main.go:4) MOVQ main..autotmp_2+24(SP), DI // 应该是设置某个条件码寄存器的标志位(暂时不用管) 0x005f 00095 (main.go:4) TESTB AL, (DI) // 将变量x的地址(在SP栈指针向上偏移32字节的位置)复制给CX寄存器 0x0061 00097 (main.go:4) MOVQ main.&x+32(SP), CX // DX寄存器用来存储闭包的环境指针 0x0066 00102 (main.go:4) LEAQ 8(DI), DX 0x006a 00106 (main.go:4) PCDATA $0, $-2 // 这段是内存写屏障相关的汇编,有点看不懂 0x006a 00106 (main.go:4) CMPL runtime.writeBarrier(SB), $0 0x0071 00113 (main.go:4) JEQ 117 0x0073 00115 (main.go:4) JMP 123 // 设置闭包的环境指针(CX寄存器存储的是x的地址) 0x0075 00117 (main.go:4) MOVQ CX, 8(DI) 0x0079 00121 (main.go:4) JMP 135 0x007b 00123 (main.go:4) MOVQ DX, DI 0x007e 00126 (main.go:4) NOP 0x0080 00128 (main.go:4) CALL runtime.gcWriteBarrierCX(SB) 0x0085 00133 (main.go:4) JMP 135 0x0087 00135 (main.go:4) PCDATA $0, $-1 // test()最终返回的是闭包对象的地址 0x0087 00135 (main.go:4) MOVQ main..autotmp_2+24(SP), AX 0x008c 00140 (main.go:4) MOVQ AX, main.~r0+16(SP) ······main.test.func1 STEXT size=214 args=0x0 locals=0x68 funcid=0x0 align=0x0 0x0000 00000 (main.go:6) TEXT main.test.func1(SB), NEEDCTXT|ABIInternal, $104-0 ······ // DX寄存器存储的是闭包对象的起始地址 // 从闭包内存空间的第8个字节的位置读取变量x的地址到CX寄存器 0x000e 00014 (main.go:4) MOVQ 8(DX), CX // 将变量x的地址拷贝到栈内存中(SP指针向上偏移8字节的位置) 0x0012 00018 (main.go:4) MOVQ CX, main.&x+8(SP) // 返回值初始化 0x0017 00023 (main.go:4) MOVQ $0, main.~r0(SP) // 把变量x的地址放到CX寄存器中(获取变量x的地址,为写入x做准备) 0x001f 00031 (main.go:5) MOVQ main.&x+8(SP), CX // 将立即数2写入到变量x所在的内存地址(该位置在堆内存中) 0x0024 00036 (main.go:5) MOVQ $2, (CX) // 再次获取变量x的地址,准备读取x的值 0x002b 00043 (main.go:6) MOVQ main.&x+8(SP), CX // CX寄存器指向的内存地址(变量x的地址)中的值(立即数2)复制给AX寄存器 0x0030 00048 (main.go:6) MOVQ (CX), AX // 把AX寄存器的值(变量x)复制到函数返回寄存器(r0)中 0x0033 00051 (main.go:6) MOVQ AX, main.~r0(SP) ······上面分析完了大部分的汇编代码。汇编代码晦涩难懂,既要懂得相关语法,又要把握好程序执行基本脉络。
从汇编中可以看出,闭包其实就是一个函数指针和一个环境指针。环境指针指向变量 x 的地址,由于变量 x 最初分配在栈上,当 test()调用完成后变量 x 就会被销毁。于是编译器为了能让环境指针一直指向变量 x,就将变量 x 从栈内存拷贝到了堆内存,以此来延长变量 x 的生命周期。除此之外,一个指向匿名函数的指针也被赋值给了闭包的函数指针,这样只要调用闭包,相对应的函数就能执行。
整体逻辑虽然并不算复杂,但是其中的细节确实非常多,想要完全弄明白也不容易(堆栈内存分配和写屏障问题)。
闭包中的“延迟求值”特性
只要从汇编角度理解了闭包,其实闭包的“延迟求值”特性也不难理解。主要是以前我不太理解这个延迟求值的本质是什么。下面看测试代码:
func test() []func() { var s []func()//定义一个切片s,s的类型为func() for i := 0; i < 2; i++ { s = append(s, func() { fmt.Println(i) }) } return s}func main() { for _, value := range test() {//遍历test()的返回值 value() }}这段代码的执行结果我最初以为是 0 和 1,但是实际的执行结果是输出了两个 2。
现在结合闭包的底层原理很快就能知道为什么是两个 2。切片 s 为 func()类型的切片,使用 append 将匿名函数放在切片 s 中。由于匿名函数引用了外部变量 i,因此这个匿名函数和变量 i 构成了一个闭包。所以这个匿名函数并不会立即执行,只有在调用闭包的时候,匿名函数才会执行。
现在看 main 函数中的执行逻辑:首先执行 test()函数,得到一个 func()类型的切片,该切片中包含两个匿名函数,接下来就是遍历切片。在第一次迭代中执行闭包的时候,需要打印变量 i 的值,尽管 test()函数已经执行完毕(test 函数的栈帧已经被销毁),但是由于变量 i 和匿名函数构成了闭包,所以编译器将变量 i 从栈内存拷贝到了堆内存,那现在 i 的值是多少呢?就是 test 函数栈在销毁前最终的 i 值,也就是 2。因此两个匿名函数都使用了堆内存中的同一个 i,并将其打印出来。
那么如果我想让执行结果为 0 和 1,该如何修改代码呢?其实也很简单,问题的根源在于 test()函数执行完成后变量 i 从栈内存拷贝到了堆内存,所以堆中只是记录了 i 的最终状态,那么如果我们能够把 test()函数中 for 循环中的每个变量 i 都记录下来,那么问题也就解决了。所以解决办法是在匿名函数外部,for 循环内部定义一个变量,将 i 值记录到这个变量中。这样每次 for 循环都会创建一个新的变量,每个变量记录当前的 i 值,最终这些变量都会逃逸到堆区,最后被匿名函数引用。代码如下:
func test() []func() { var s []func() for i := 0; i < 2; i++ { //虽然都是x,但是每个x的地址都不同,最后所有的x都会逃逸到堆区 x := i s = append(s, func() { println(x) }) } return s}func main() { result := test() for _, value := range result { value() }}下面是使用命令go tool compile -N -l -m=2 main.go进行逃逸分析:
[deep@binary test]$ go tool compile -N -l -m=2 main.gomain.go:9:17: func literal escapes to heap:main.go:9:17: flow: {heap} = &{storage for func literal}:main.go:9:17: from func literal (spill) at main.go:9:17main.go:9:17: from append(s, func literal) (call parameter) at main.go:9:13main.go:8:3: test capturing by value: x (addr=false assign=false width=8)main.go:10:15: x escapes to heap:main.go:10:15: flow: {storage for ... argument} = &{storage for x}:main.go:10:15: from x (spill) at main.go:10:15main.go:10:15: from ... argument (slice-literal-element) at main.go:10:15main.go:10:15: flow: {heap} = {storage for ... argument}:main.go:10:15: from ... argument (spill) at main.go:10:15main.go:10:15: from fmt.Println(... argument...) (call parameter) at main.go:10:15main.go:9:17: func literal escapes to heapmain.go:10:15: ... argument does not escapemain.go:10:15: x escapes to heap可以看到变量 x 和闭包函数(func literal)逃逸到了堆上(x escapes to heap和func literal escapes to heap),gc 会负责管理这些在堆上分配的内存。