1. 函数在 Go 语言中属于第一类值(First Class Value),该类型的值可以作为函数的参数返回值,也可以赋给变量
  2. 当把一个函数赋值给某个变量后,这个变量就被称为 Function Value。
  3. 声明一个 Function Value 变量的示例代码如下:
var fn func(a, b int) int
  1. 其中 fn 就是个 Function Value 变量,它的类型是 func(int, int) int。
  2. Function Value 可以像一般函数那样被调用,在使用体验上非常类似于 C 语言中的函数指针。
  3. Function Value 本质上是不是函数指针呢?
  4. 本节会分析 Function Value 和函数指针的实现原理,还有闭包的实现原理,以及 Function Value 是如何支持闭包的。

函数指针

  1. 熟悉 C 语言的读者应该有过使用函数指针的经验,函数指针存储的都是地址,只不过不是指向某种类型的数据。
  2. 而是指向代码段中某个函数的第一条指令,如图所示。

Function Value 分析

  1. 准备一个 go 文件并写入,示例代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

func main() {
    println(helper(nil, 0, 0))
}

//go:noinline
func helper(fn func(int, int) int, a, b int) int {
    return fn(a, b)
}
  1. 依然把 Function Value 的调用隔离在一个函数中,以便于分析。main.helper反编译代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ go build -gcflags="-N -l" -o ./h1 myzx.cn/helium
$ go tool objdump -S -s '^main.helper$' ./h1 
TEXT main.helper(SB) /mnt/hgfs/workspace/helium/main.go
func helper(fn func(int, int) int, a, b int) int {
  0x455240      493b6610            CMPQ 0x10(R14), SP  # 栈增长判断
  0x455244      7650                JBE 0x455296        
  0x455246      4883ec28            SUBQ $0x28, SP        
  0x45524a      48896c2420          MOVQ BP, 0x20(SP)    
  0x45524f      488d6c2420          LEAQ 0x20(SP), BP    
  0x455254      4889442430          MOVQ AX, 0x30(SP)   # fn
  0x455259      48895c2438          MOVQ BX, 0x38(SP)   # a
  0x45525e      48894c2440          MOVQ CX, 0x40(SP)   # b
  0x455263      48c744241000000000  MOVQ $0x0, 0x10(SP) # return int
    return fn(a, b)
  0x45526c      488b442438          MOVQ 0x38(SP), AX   # AX=a
  0x455271      488b5c2440          MOVQ 0x40(SP), BX   # BX=b
  0x455276      488b542430          MOVQ 0x30(SP), DX   # DX=fn
  # 这里看出 Function Value 是一个二级指针
  0x45527b      488b0a              MOVQ 0(DX), CX      # CX=fn.fn
  0x45527e      6690                NOPW            
  # DX 寄存器存储的是 fn,也就是上下文
  # CALL CX; 指令说明,CX寄存器最终存储的是实际函数的地址
  0x455280      ffd1                CALL CX             # CALL fn.fn
  0x455282      4889442418          MOVQ AX, 0x18(SP)    
  0x455287      4889442410          MOVQ AX, 0x10(SP)    
  0x45528c      488b6c2420          MOVQ 0x20(SP), BP    
  0x455291      4883c428            ADDQ $0x28, SP        
  0x455295      c3                  RET            
func helper(fn func(int, int) int, a, b int) int {
  0x455296      4889442408          MOVQ AX, 0x8(SP)            
  0x45529b      48895c2410          MOVQ BX, 0x10(SP)            
  0x4552a0      48894c2418          MOVQ CX, 0x18(SP)            
  0x4552a5      e8b6ccffff          CALL runtime.morestack_noctxt.abi0(SB)    
  0x4552aa      488b442408          MOVQ 0x8(SP), AX            
  0x4552af      488b5c2410          MOVQ 0x10(SP), BX            
  0x4552b4      488b4c2418          MOVQ 0x18(SP), CX            
  0x4552b9      eb85                JMP main.helper(SB)
  1. 通过上述逻辑,可以确定Function Value确实是个指针,而且是个两级指针
  2. 如图所示,Function Value 不直接指向目标函数,而是一个目标函数的指针。

闭包

  1. 说到Go语言的闭包,比较直观的感受就是个有状态的 Function Value。
  2. Go语言中比较典型的闭包场景就是在某个函数内定义了另一个函数,内层函数使用了外层函数的局部变量,并且内层函数最终被外层函数作为返回值返回。
  3. 代码如下:
1
2
3
4
5
func mc(n int) func() int {
    return func() int {
        return n
    }
}
  1. 每次调用 mc() 函数都会返回一个新的闭包,闭包记住了参数的值,所以是有状态的。
  2. 基于目前对函数栈帧的了解,函数栈帧随着函数返回而销毁,不能用来保存状态。
  3. 研究函数指针和 Function Value 的时候也没有发现哪里用来保存状态,所以这里就有个问题,闭包的状态保存在哪里呢?

闭包对象

  1. 为了摘清楚这个问题,先来尝试一下反编译,从汇编代码中找答案,main.mc反编译代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ go build -gcflags="-N -l" -o ./h1 myzx.cn/helium
$ go tool objdump -S -s '^main.mc$' ./h1 
TEXT main.mc(SB) /mnt/hgfs/workspace/helium/main.go
func mc(n int) func() int {
  0x455220      493b6610            CMPQ 0x10(R14), SP  # 栈增长判断
  0x455224      765b                JBE 0x455281        
  0x455226      4883ec28            SUBQ $0x28, SP        
  0x45522a      48896c2420          MOVQ BP, 0x20(SP)    
  0x45522f      488d6c2420          LEAQ 0x20(SP), BP    
  # 参数栈空间分配
  0x455234      4889442430          MOVQ AX, 0x30(SP)   # n
  # 返回值栈空间分配
  0x455239      48c744241000000000  MOVQ $0x0, 0x10(SP) # return func() int
    return func() int {
  # 0x7497(IP) 是 Function Value 的结构元类型
  # type funcval struct {fn unsafe.Pointer, n int}
  0x455242      488d0597740000      LEAQ 0x7497(IP), AX # Function Value 结构的元类型
  # 创建 funcval 结构需要的内存空间,是堆分配
  0x455249      e8f25cfbff          CALL runtime.newobject(SB)  # AX=&funcval
  0x45524e      4889442418          MOVQ AX, 0x18(SP)        
  # 这里main.mc.func1(SB)是mc返回的闭包在代码段的地址
  0x455253      488d0d46000000      LEAQ main.mc.func1(SB), CX  # CX=main.mc.func1(SB)
  0x45525a      488908              MOVQ CX, 0(AX)              # funcval.fn=main.mc.func1(SB)
  0x45525d      488b4c2418          MOVQ 0x18(SP), CX        
  0x455262      8401                TESTB AL, 0(CX)            
  0x455264      488b542430          MOVQ 0x30(SP), DX           # DX=n    
  # 可以看出这里捕获了变量n,并且是拷贝捕获
  0x455269      48895108            MOVQ DX, 0x8(CX)            # funcval.n = n
  0x45526d      488b442418          MOVQ 0x18(SP), AX           # AX=&funcval
  0x455272      4889442410          MOVQ AX, 0x10(SP)        
  0x455277      488b6c2420          MOVQ 0x20(SP), BP        
  0x45527c      4883c428            ADDQ $0x28, SP            
  0x455280      c3                  RET                
func mc(n int) func() int {
  0x455281      4889442408          MOVQ AX, 0x8(SP)            
  0x455286      e8d5ccffff          CALL runtime.morestack_noctxt.abi0(SB)    
  0x45528b      488b442408          MOVQ 0x8(SP), AX            
  0x455290      eb8e                JMP main.mc(SB)
  1. 根据上面代码可以推断出Function Value动态分配的对象的类型。
  2. 应该是一个 struct 类型,第1个字段是函数地址第2个字段是 int 类型,代码如下:
1
2
3
4
struct {
    F uintptr
    n int
}
  1. 说明编译器识别出了闭包这种代码模式,并且自动定义了这个 struct 类型进行支持,出于面向对象编程中把数据称为对象的习惯,后文中就把这种 struct 称为闭包对象。
  2. 闭包对象的成员可以进一步划分,第1个字段F用来存储目标函数的地址,这在所有的闭包对象中都是一致的,后文中将这个目标函数称为闭包函数。
  3. 从第2个字段开始,后续的字段称为闭包的捕获列表,也就是内层函数中用到的所有定义在外层函数中的变量。
  4. 编译器认为这些变量被闭包捕获了,会把它们追加到闭包对象的 struct 定义中。
  5. 上例中只捕获了一个变量 n,如果捕获的变量增多,struct 的捕获列表也会加长。
  6. 一个捕获两个变量的闭包示例代码如下:
1
2
3
4
5
func mc(n, m int) func() (int, int) {
    return func() (int, int) {
        return n, m
    }
}
  1. 上述代码对应的闭包对象定义代码如下:
1
2
3
4
5
struct {
    F uintptr
    n int
    m int
}

看到闭包

  1. 通过反编译来逆向推断闭包对象的结构还是比较烦琐的,如果能有一种方法,能够直观看到闭包对象的结构定义,那真是再好不过了。
  2. 根据之前的探索,已经知道 Go 序在运行阶段会通过 runtime.newobject() 函数动态匹配闭包对象。
  3. Go 源码中 newobject() 函数的原型如下:
func newobject(typ *_type) unsafe.Pointer
  1. 函数的返回值是个指针,也就是新分配的对象的地址,参数是个 _type 类型的指针。
  2. 通过源码可以得知这个 _type 是个 struct,在 Go 语言的 runtime 包中被用来描述一个数据类型,通过它可以找到目标数据类型的大小,对齐边界,类型名称等。
  3. 假如能够获得传递给 runtime.newobjet() 函数的类型元数据指针 typ,再通过反射进行解析,就能打印出闭包对象的结构定义了。那如何才能获得这个 typ 参数呢?
  4. C语言中有种常用的函数 Hook 技术,就是在运行阶段将目标函数头部的代码替换为一条跳转指令,跳转到一个新的函数。
  5. 在 x86 平台上就是在进程地址空间中找到要 Hook 的函数,将其头部替换为一条 JMP 指令,同时指定 JMP 指令要跳转到的新函数的地址。
  6. 这项技术在 Go 程序中依然适用,可以用一个自己实现的函数换掉 runtime.newobject() 函数,在这个函数中就能获得 typ 参数并进行解析了。
  7. 还有一个问题是 runtime.newobiect() 函数属于未导出的函数,在runtime包外无法访问。
  8. 这一点可以通过 linkname 机制来绕过,在当前包中声明一个类似的函数,让链接器将其链接到runtime.newobject()函数即可。
  9. 本书使用开源模块 github.com/fengyoulin/hookingo 实现运行阶段函数替换,打印闭包对象结构的完整代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "github.com/fengyoulin/hookingo"
    "reflect"
    "unsafe"
)

var hno hookingo.Hook

//go:linkname newobject runtime.newobject
func newobject(typ unsafe.Pointer) unsafe.Pointer

//go:linkname newobject2 runtime.mallocgc
func newobject2(size uintptr, typ unsafe.Pointer, b bool) unsafe.Pointer

func fno(typ unsafe.Pointer) unsafe.Pointer {
    t := reflect.TypeOf(0) // type Type interface{}
    // 这里为什么赋值的是下标1?接下来我们分析为什么是1而不是0下标。
    // 根据后面分析确实是修改下标1,因为t.String()需要用到1这个下标的数据。
    (*(*[2]unsafe.Pointer)(unsafe.Pointer(&t)))[1] = typ // 相当于反射了闭包类型
    println(t.String())
    //fn, ok := hno.Origin().(func(typ unsafe.Pointer) unsafe.Pointer)
    //if ok {
    //    return fn(typ) // 调用原runtime.newobject
    //}
    //println("ok", ok)
    //os.Exit(1)
    println(*(*uintptr)(unsafe.Pointer(typ)))
    return newobject2(*(*uintptr)(typ), typ, true)
}

// 创建一个闭包,make closure
func mc(start int, text string) func() string {
    // 这里 start 需要堆分配
    // func() string 也需要堆分配
    return func() string {
        l := len(text)
        s := start % l
        r := text[s:]
        start++
        return r
    }
}

func main() {
    var err error
    hno, err = hookingo.Apply(newobject, fno) // 应用钩子,替换函数
    if err != nil {
        panic(err)
    }
    f := mc(10, "hello, closure!")
    println(f())
}
$ C:\Users\Helium\go\bin\go1.17.exe run -gcflags -l .   # go1.17 版本
int         # 打印的是mc的start堆分配
8           # int 占用内存大小
struct { F uintptr; text string; start *int }
32
sure!
$ go run -gcflags -l .                                  # go1.20.3版本
int
8
struct { F uintptr; text string; start *int }
32
sure!
  1. 因为start会被修改,所以捕获地址造成堆分配,因此结果第一行打印了一个int
  2. 第二行的struct就是闭包结构的类型,这是通过类型元数据直接获取到的,是由编译器构造的结构类型。
  3. 我们抄部分fno()函数的相关代码:typ unsafe.Pointer:来自类型的*_type类型数据。
1
2
3
4
5
6
7
func fno(typ unsafe.Pointer) unsafe.Pointer {
    t := reflect.TypeOf(0) // type Type interface{}
    // 这里为什么赋值的是下标1?接下来我们分析为什么是1而不是0下标。
    (*(*[2]unsafe.Pointer)(unsafe.Pointer(&t)))[1] = typ // 相当于反射了闭包类型
    println(t.String())
    // ... ...
}
  1. 分析reflect.TypeOf()函数如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type {
    // emptyInterface 是空接口的内存结构
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    // eface.typ 这里直接取了 typ 字段,没有要word字段
    // 通过分析toType()函数可知,把eface.typ放入了非空接口Type中。
    // 因此必须要分析非空接口中存储的是什么?非空接口内存布局
    // type iface struct {
    //      tab *itab               // 类型元数据
    //      data *unsafe.Pointer    // 装箱的数据
    // }
    // 因此 tab *itab 装载的是 *rtype 类型结构,也就是结构体指针
    // 按照传入参数0这里可以看出应该是int的元类型
    // 重要的是 data,这存储的是 *rtype 这个指针数据,类型反射就是依靠这个data。
    // 因为我们调用Type非空接口的所有方法接收器的参数都是 *rtype,这也就是为什么修改下标1的原因。
    // 我们在调用t.String()方法时,需要的时data这个是我们想要的类型即可。
    return toType(eface.typ)
}

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype        // 存储着类型结构
    word unsafe.Pointer
}

  1. 分析toType()函数如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

调用闭包

  1. 闭包函数在被调用的时候,必须得到当前闭包对象的地址才能访问其中的捕获列表,这个地址是如何传递的呢?
  2. 调用者在调用 Function Value 的时候只是像调用一个普通函数那样传递了声明的参数,如果 Function Value 背后是个闭包函数,则无法通过栈上的参数得到闭包对象地址。
  3. 除非编译器传递了一个隐含的参数,这个参数如果通过栈传递,那就改变了函数的原型,这样就会造成不一致,是行不通的。
  4. 还是通过反汇编来看一下闭包函数是从哪里得到的这个地址,先来构造闭包,代码如下:
1
2
3
4
5
func mc(n int) func int {
    return func() int {
        return n
    }
}
  1. 根据前面的探索,可以确定闭包对象的结构定义代码如下:
1
2
3
4
struct {
    F uintptr
    n int
}
  1. 反编译闭包函数得到的汇编代码如下:
    1. 第7行,将 DX 寄存器用作基址,再加上位移 8,把该地址处的值复制到 CX 寄存器中。
    2. 第8行,把 CX 寄存器值复制给闭包函数的返回值。
    3. 第11行,把返回值放入AX寄存器中。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ go tool objdump -S -s '^main.mc.func1$' ./h1
TEXT main.mc.func1(SB) /mnt/hgfs/workspace/helium/main.go
    return func() int {
  0x4552a0      4883ec18            SUBQ $0x18, SP        
  0x4552a4      48896c2410          MOVQ BP, 0x10(SP)    
  0x4552a9      488d6c2410          LEAQ 0x10(SP), BP    
  0x4552ae      488b4a08            MOVQ 0x8(DX), CX    # CX=0x8(DX)
  0x4552b2      48894c2408          MOVQ CX, 0x8(SP)    
  0x4552b7      48c7042400000000    MOVQ $0x0, 0(SP)    
        return n
  0x4552bf      488b442408          MOVQ 0x8(SP), AX    # AX=0x8(DX)
  0x4552c4      48890424            MOVQ AX, 0(SP)        
  0x4552c8      488b6c2410          MOVQ 0x10(SP), BP    
  0x4552cd      4883c418            ADDQ $0x18, SP        
  0x4552d1      c3                  RET
  1. 显然,DX寄存器存储的就是闭包对象的地址,调用者负责在调用之前把闭包对象的地址存储到 DX 寄存器中,跟 C++ 中的 thiscall非常类似。
  2. 之前有很多读者在反编译 Function Value 调用代码时,总会看到为 DX 寄存器赋值,并为此感到疑惑,这就是原因。
  3. 调用者不必区分是不是闭包、有没有捕获列表,实际上也区分不了,只能统一作为闭包来处理,所以总要通过 DX 传递地址。
  4. 如果 Function Value 背后不是闭包,这个地址就不会被用到,也不会造成什么影响。

闭包与变量逃逸

  1. 变量逃逸跟闭包之间的关系很密切,因为 Function Value 本身就是个指针,编译器也可以按照同样的方式来分析 Function Value 有没有逃逸。
  2. 如果 Function Value 没有逃逸那就可以不用在堆上分配闭包对象了,分配在栈上即可。
  3. 使用一个示例进行验证,代码如下:
1
2
3
4
5
6
func sc(n int) int {
    f := func() int {
        return n
    }
    return f()
}
  1. 代码逻辑过于简单,为了避免闭包函数被编译器优化掉,编译时需要禁用内联优化,命令如下:$ go build -gcflags='-l'
  2. 再来反编译 sc() 函数,main.sc反编译命令及输出结果如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ go build -gcflags="-l -N" -o ./h1 myzx.cn/helium
$ go tool objdump -S -s '^main.sc$' ./h1
TEXT main.sc(SB) /mnt/hgfs/workspace/helium/main.go
func sc(n int) int {
  0x455220      493b6610            CMPQ 0x10(R14), SP
  0x455224      7664                JBE 0x45528a
  0x455226      4883ec38            SUBQ $0x38, SP
  0x45522a      48896c2430          MOVQ BP, 0x30(SP)
  0x45522f      488d6c2430          LEAQ 0x30(SP), BP
  0x455234      4889442440          MOVQ AX, 0x40(SP)   # 分配参数空间
  0x455239      48c7042400000000    MOVQ $0x0, 0(SP)    # 分配返回值空间
    f := func() int {
  0x455241      440f117c2410        MOVUPS X15, 0x10(SP)
  0x455247      488d542410          LEAQ 0x10(SP), DX   # &funcval
  0x45524c      4889542428          MOVQ DX, 0x28(SP)
  0x455251      8402                TESTB AL, 0(DX)
  0x455253      488d0546000000      LEAQ main.sc.func1(SB), AX
  0x45525a      4889442410          MOVQ AX, 0x10(SP)   # funcval.fn 0x10(SP)
  0x45525f      8402                TESTB AL, 0(DX)
  0x455261      488b442440          MOVQ 0x40(SP), AX   # 参数n的值 0
  0x455266      4889442418          MOVQ AX, 0x18(SP)   # funcval.data 0x18(SP) 0
  0x45526b      4889542420          MOVQ DX, 0x20(SP)
    return f()
  0x455270      488b442410          MOVQ 0x10(SP), AX
  0x455275      ffd0                CALL AX
  0x455277      4889442408          MOVQ AX, 0x8(SP)
  0x45527c      48890424            MOVQ AX, 0(SP)
  0x455280      488b6c2430          MOVQ 0x30(SP), BP
  0x455285      4883c438            ADDQ $0x38, SP
  0x455289      c3                  RET
func sc(n int) int {
  0x45528a      4889442408          MOVQ AX, 0x8(SP)
  0x45528f      e8ccccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x455294      488b442408          MOVQ 0x8(SP), AX
  0x455299      eb85                JMP main.sc(SB)

函数

  1. 在Go语言中函数属于头等对象,可以被当作参数传递、也可以作为函数返回值绑定到变量
  2. Go语言称这样的参数、返回值和变量为Function Value
  3. Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体,函数变量存储的是*funcval类型,也就是funcval结构体的地址。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 闭包是函数类型funcType
// 闭包捕获的变量,系统维护着一个关系列表,根据这个列表能正确的找到捕获的变量类型大小等信息
type funcval struct {
    fn uintptr  // 指向程序的代码段
    // variable-size, fn-specific data here
}
// 紧接着funcval结构体后面内存地址是捕获的的变量列表
// 注意捕获的是变量的地址,使用的却是变量的值
// 闭包中捕获的如果是指针类型那么是引用,是普通类型那么是拷贝

// f -> *funcval
  1. 这个结构体从定义上看只有一个地址,这个地址才是函数的指令入口。一个Function Value是以下图所示形式存在的。

闭包

  1. Closure:维基百科
    • 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
    • 环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。
    • 闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
  2. 所以像下面这个例子:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func create() func(){
    c := 2  // c会被分配在栈上,以拷贝形式被捕获
    return func(){
        // 【由于c变量后面没有变化,所以这里捕获变成拷贝c的值2就行,不需要捕获c的地址】
        fmt.Println(c)    
    }
}

func main(){
    // f1 {
    //      fn ---> 闭包函数地址
    //      2  ---> 拷贝c的值即可(不需要捕获c的地址),这里2
    // }
    f1 := create()
    f2 := create()
    f1()
    f2()
    
    // 闭包函数的内存结构布局
    s := **(**struct{
        fn uintptr  // 指向函数代码地址
        data1 int   // 捕获变量
    })(unsafe.Pointer(&f1))

    fmt.Printf("%#v\n", s)    // struct {fn uintptr;data1 int}{fn:0x47f520, data1:2}
}
  1. create函数的返回值是一个函数,并且引用了其外层函数定义的局部变量c
  2. 而且,即便create函数结束,依然可以通过f1f2正常执行这个函数并使用定义在create内部的变量c
  3. 所以这个返回值符合闭包的定义,而这个自由变量c,通常被称为捕获变量
  4. 虽然create函数的返回值函数形成闭包,但是Go语言里并没有把闭包从Function Value中特别区分出来。
  5. 在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已
  6. 这些捕获变量就是它的捕获列表,就放在对应的funcval结构体的后面。
  7. 所以上例中,f1f2的内存布局如下图所示:

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

闭包捕获变量值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

func main() {
    f := create()
    _ = f()
}

//go:noinline
func create() func() int {
    c := 2
    // 堆分配 struct { F uintptr; c int }
    return func() int {
        // 注意这里的c变量不需要被捕获
        // c没有更改,只需要拷贝变量c的值即可
        return c
    }
}
  1. main 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
  0x4551e0      493b6610        CMPQ 0x10(R14), SP      # 栈增长判断
  0x4551e4      7629            JBE 0x45520f
  0x4551e6      4883ec10        SUBQ $0x10, SP          # main.main栈分配
  0x4551ea      48896c2408      MOVQ BP, 0x8(SP)
  0x4551ef      488d6c2408      LEAQ 0x8(SP), BP
    f := create()
  0x4551f4      e827000000      CALL main.create(SB)    # AX返回&funcval
  0x4551f9      48890424        MOVQ AX, 0(SP)
    _ = f()
  0x4551fd      488b08          MOVQ 0(AX), CX          # CX=funcval.fn
  0x455200      4889c2          MOVQ AX, DX             # DX=&funcval
  0x455203      ffd1            CALL CX
}
  0x455205      488b6c2408      MOVQ 0x8(SP), BP
  0x45520a      4883c410        ADDQ $0x10, SP
  0x45520e      c3              RET
func main() {
  0x45520f      e84ccdffff      CALL runtime.morestack_noctxt.abi0(SB)
  0x455214      ebca            JMP main.main(SB)
  1. create 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func create() func() int {
  0x455220        493b6610            CMPQ 0x10(R14), SP  # 栈增长判断
  0x455224        765f                JBE 0x455285
  0x455226        4883ec30            SUBQ $0x30, SP      # 分配create栈空间
  0x45522a        48896c2428          MOVQ BP, 0x28(SP)
  0x45522f        488d6c2428          LEAQ 0x28(SP), BP
  0x455234        48c744241800000000  MOVQ $0x0, 0x18(SP) # 临时返回空间8字节,*funcval
    c := 2
  0x45523d        48c744241002000000  MOVQ $0x2, 0x10(SP) # 参数 c
    return func() int {
  # AX=0x7493(IP),funcval结构体类型的_type类型指针位置,内存大小16B
  0x455246        488d0593740000      LEAQ 0x7493(IP), AX 
  # 根据*_type进行堆分配,AX=&funcval
  0x45524d        e8ee5cfbff          CALL runtime.newobject(SB)  
  0x455252        4889442420          MOVQ AX, 0x20(SP)
  # CX=main.create.func1 代码地址
  0x455257        488d0d42000000      LEAQ main.create.func1(SB), CX  
  0x45525e        488908              MOVQ CX, 0(AX)    # funcval.fn=main.create.func1
  0x455261        488b4c2420          MOVQ 0x20(SP), CX # CX=&funcval
  0x455266        8401                TESTB AL, 0(CX)
  0x455268        488b542410          MOVQ 0x10(SP), DX # DX=0x2
  0x45526d        48895108            MOVQ DX, 0x8(CX)  # funcval.data=0x2
  0x455271        488b442420          MOVQ 0x20(SP), AX # AX=&funcval
  0x455276        4889442418          MOVQ AX, 0x18(SP)
  0x45527b        488b6c2428          MOVQ 0x28(SP), BP
  0x455280        4883c430            ADDQ $0x30, SP
  0x455284        c3                  RET
func create() func() int {
  0x455285        e8d6ccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x45528a        eb94                JMP main.create(SB)
  1. main.create.func1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
TEXT main.create.func1(SB) /mnt/hgfs/g/hello1/func1.go
  return func() int {
0x4552a0      4883ec18            SUBQ $0x18, SP
0x4552a4      48896c2410          MOVQ BP, 0x10(SP)
0x4552a9      488d6c2410          LEAQ 0x10(SP), BP
0x4552ae      488b4a08            MOVQ 0x8(DX), CX    # CX=0x2
0x4552b2      48894c2408          MOVQ CX, 0x8(SP)
0x4552b7      48c7042400000000    MOVQ $0x0, 0(SP)
      // 注意这里的c变量不需要被捕获
0x4552bf      488b442408          MOVQ 0x8(SP), AX    # AX=0x2
0x4552c4      48890424            MOVQ AX, 0(SP)
0x4552c8      488b6c2410          MOVQ 0x10(SP), BP
0x4552cd      4883c418            ADDQ $0x18, SP
0x4552d1      c3                  RET
  1. main和create的栈分布。
          |   runtime.main callback
          --------------------------
  +40 +08 |   runtime.main BP
          --------------------------  BP      ---------------
  +38 +00 |   create.r *funcval               main.main栈
          --------------------------  SP      ---------------
  +30     |   main.main callback
          --------------------------
  +28     |   main.main BP
          --------------------------  BP      ---------------
  +20     |   &funcval    create.o            -> 临时funcval堆分配
          --------------------------
  +18     |   &funcval    create.r            -> create的返回值
          --------------------------
  +10     |   0x2         create.c            main.create栈
          --------------------------
  +08     |
          --------------------------
  +00     |
          --------------------------  SP      ---------------

闭包捕获变量地址
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

func main() {
    f := create()
    _ = f()
}

//go:noinline
func create() func() int {
    c := 2
    // 堆分配 struct { F uintptr; c *int }
    return func() int {
        c++   // 捕获c变量地址,在堆上分配
        return c
    }
}
  1. main 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
  0x4551e0        493b6610        CMPQ 0x10(R14), SP
  0x4551e4        7629            JBE 0x45520f
  0x4551e6        4883ec10        SUBQ $0x10, SP
  0x4551ea        48896c2408      MOVQ BP, 0x8(SP)
  0x4551ef        488d6c2408      LEAQ 0x8(SP), BP
    f := create()
  0x4551f4        e827000000      CALL main.create(SB)    # 调用函数 AX返回&funcval
  0x4551f9        48890424        MOVQ AX, 0(SP)
    _ = f()
  0x4551fd        488b08          MOVQ 0(AX), CX          # CX=funcval.fn
  0x455200        4889c2          MOVQ AX, DX             # DX=&funcval
  0x455203        ffd1            CALL CX
}
  0x455205        488b6c2408      MOVQ 0x8(SP), BP
  0x45520a        4883c410        ADDQ $0x10, SP
  0x45520e        c3              RET
func main() {
  0x45520f        e84ccdffff      CALL runtime.morestack_noctxt.abi0(SB)
  0x455214        ebca            JMP main.main(SB)
  1. create 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func create() func() int {
  0x455220        493b6610            CMPQ 0x10(R14), SP
  0x455224        0f8686000000        JBE 0x4552b0
  0x45522a        4883ec30            SUBQ $0x30, SP
  0x45522e        48896c2428          MOVQ BP, 0x28(SP)
  0x455233        488d6c2428          LEAQ 0x28(SP), BP
  0x455238        48c744241000000000  MOVQ $0x0, 0x10(SP)
    c := 2
  0x455241        488d05f84a0000      LEAQ 0x4af8(IP), AX             # AX=0x4af8(IP),int的元类型
  0x455248        e8f35cfbff          CALL runtime.newobject(SB)      # 申请内存,AX=*int
  0x45524d        4889442420          MOVQ AX, 0x20(SP)               # 变量c
  0x455252        48c70002000000      MOVQ $0x2, 0(AX)                # 给c赋值2
    return func() int {
  0x455259        488d0580740000      LEAQ 0x7480(IP), AX             # funcval的元类型,申请16B
  0x455260        e8db5cfbff          CALL runtime.newobject(SB)      # AX=&funcval
  0x455265        4889442418          MOVQ AX, 0x18(SP)
  0x45526a        488d0d4f000000      LEAQ main.create.func1(SB), CX  # CX=main.create.func1
  0x455271        488908              MOVQ CX, 0(AX)                  # funcval.fn=main.create.func1
  0x455274        488b4c2418          MOVQ 0x18(SP), CX               # CX=&funcval
  0x455279        8401                TESTB AL, 0(CX)
  0x45527b        488b542420          MOVQ 0x20(SP), DX               # DX=*int -> 2
  0x455280        488d7908            LEAQ 0x8(CX), DI                # DI=funcval.data
  0x455284        833dd51f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x45528b        7402                JE 0x45528f
  0x45528d        eb06                JMP 0x455295
  0x45528f        48895108            MOVQ DX, 0x8(CX)                # funcval.data=*int -> 2
  0x455293        eb07                JMP 0x45529c
  0x455295        e8a6d0ffff          CALL runtime.gcWriteBarrierDX(SB)
  0x45529a        eb00                JMP 0x45529c
  0x45529c        488b442418          MOVQ 0x18(SP), AX               # AX=&funcval
  0x4552a1        4889442410          MOVQ AX, 0x10(SP)
  0x4552a6        488b6c2428          MOVQ 0x28(SP), BP
  0x4552ab        4883c430            ADDQ $0x30, SP
  0x4552af        c3                  RET
func create() func() int {
  0x4552b0        e8abccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x4552b5        e966ffffff          JMP main.create(SB)
  1. main.create.func1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
TEXT main.create.func1(SB) /mnt/hgfs/g/hello1/func1.go
  return func() int {
 0x4552c0     4883ec18            SUBQ $0x18, SP
 0x4552c4     48896c2410          MOVQ BP, 0x10(SP)
 0x4552c9     488d6c2410          LEAQ 0x10(SP), BP
 0x4552ce     488b4a08            MOVQ 0x8(DX), CX
 0x4552d2     48894c2408          MOVQ CX, 0x8(SP)
 0x4552d7     48c7042400000000    MOVQ $0x0, 0(SP)
      c++     // 捕获c变量地址,在堆上分配
 0x4552df     488b4c2408          MOVQ 0x8(SP), CX
 0x4552e4     488b09              MOVQ 0(CX), CX
 0x4552e7     488b542408          MOVQ 0x8(SP), DX
 0x4552ec     48ffc1              INCQ CX
 0x4552ef     48890a              MOVQ CX, 0(DX)
      return c
 0x4552f2     488b4c2408          MOVQ 0x8(SP), CX
 0x4552f7     488b01              MOVQ 0(CX), AX
 0x4552fa     48890424            MOVQ AX, 0(SP)
 0x4552fe     488b6c2410          MOVQ 0x10(SP), BP
 0x455303     4883c418            ADDQ $0x18, SP
 0x455307     c3                  RET
  1. main和create的栈分布。
          |   runtime.main callback
          -------------------------
  +40 +08 |   runtime.main BP
          -------------------------   BP      ------------
  +38 +00 |   &funcval        c.r             main.main栈
          -------------------------   SP      ------------
  +30     |   main.main callback
          -------------------------
  +28     |   main.main BP
          -------------------------   BP      ------------
  +20     |   *int    -> 2    c
          -------------------------
  +18     |   &funcval        c.o
          -------------------------
  +10     |   &funcval        c.r
          -------------------------
  +08     |
          -------------------------
  +00     |
          -------------------------   SP      ------------

调用

  1. 通过Function Value调用函数时,会把对应的funcval结构体地址存入特定寄存器,如amd64平台使用的是DX寄存器,参看上面的汇编确实使用DX寄存器存储的。
  2. 继续使用闭包的示例,通过f1调用闭包函数时,会把f1存储的funcval结构体地址存入寄存器DX,这样在闭包函数的指令中就可以通过这个寄存器存储的地址加上8字节的偏移,就找到f1的捕获变量了。
  3. 同样的,通过f2调用闭包函数时,会把f2存储的funcval结构体地址存入寄存器,闭包函数执行时找到的就是f2的捕获变量了。
    • 如果是没有捕获列表的Function Value,直接忽略这个寄存器即可。
    • 通过这样的方式,Go语言实现了对Function Value的统一调用。

静态分配

  1. 对于没有捕获列表的Function Value,如果多个变量关联到同一个函数,编译器会做出优化,让它们共用一个funcval结构体。
  2. 注意 funcval 后面存储的是捕获的参数,实际传参不在这里,在调用该函数时在调用栈上。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func A(i int) {
    i++
    fmt.Println(i)
}  

func B(){
    f1 := A
    f1(1)
}

func C(){
    f2 := A
    f2(1)
}
  1. 像上面这种情况,编译阶段会创建一个funcval结构体放到只读数据段,而执行阶段,f1f2都会使用它。

静态分配验证
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func main() {
    f1 := A
    f1(1)
}

//go:noinline
func A(i int) {
    i++
}
  1. main 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
  0x4551e0        493b6610            CMPQ 0x10(R14), SP
  0x4551e4        7632                JBE 0x455218
  0x4551e6        4883ec18            SUBQ $0x18, SP
  0x4551ea        48896c2410          MOVQ BP, 0x10(SP)
  0x4551ef        488d6c2410          LEAQ 0x10(SP), BP
    f1 := A
  0x4551f4        488d15251d0100      LEAQ 0x11d25(IP), DX    # DX=&funcval
  0x4551fb        4889542408          MOVQ DX, 0x8(SP)
    f1(1)
  0x455200        488b0d191d0100      MOVQ 0x11d19(IP), CX    # CX=funcval.fn,函数A的地址
  0x455207        b801000000          MOVL $0x1, AX           # AX=0x1,参数
  0x45520c        ffd1                CALL CX                  
}
  0x45520e        488b6c2410          MOVQ 0x10(SP), BP
  0x455213        4883c418            ADDQ $0x18, SP
  0x455217        c3                  RET
func main() {
  0x455218        e843cdffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x45521d        ebc1                JMP main.main(SB)
  1. A 函数汇编代码。
1
2
3
4
5
6
7
8
TEXT main.A(SB) /mnt/hgfs/g/hello1/func1.go
func A(i int) {
  0x455220        4889442408      MOVQ AX, 0x8(SP)
    i++
  0x455225        48ffc0          INCQ AX
  0x455228        4889442408      MOVQ AX, 0x8(SP)
}
  0x45522d        c3              RET
  1. main和A的栈分布。
          |   runtime.main callback
          -------------------------------
  +10     |   runtime.main BP
          ------------------------------- BP
  +08     |   0x11d25(IP)         A
          -------------------------------
  +00     |
          ------------------------------- SP

捕获列表

  1. 因为捕获列表需要由闭包对象各自持有,所以有捕获列表的Function Value要到执行阶段才会在堆上分配对应的funcval结构体以及捕获列表空间。
  2. 但是,捕获列表里存什么?直接拷贝捕获变量值吗?才没有那么简单。
  3. 闭包捕获的变量要在闭包函数和外层函数中表现一致,如果单纯值拷贝,就无法保证这一点,所以编译器针对捕获变量的不同情况分别做出了不同的处理。
    1. 捕获变量【除了初始化赋值外在任何地方都没有被修改过,那就可以直接拷贝值】,因为它不会再变化。
    2. 捕获变量除了初始化赋值外,还被修改过,就要再细分了。

捕获局部变量

  1. 这个例子中,被捕获的是局部变量i,除了初始化赋值外还被修改过,所以局部变量i改为堆分配,栈上只存一个地址。
  2. 在这个示例中,为了让被捕获的局部变量在闭包函数和外层函数中保持一致,本该在栈上分配的局部变量被分配到堆上,这其实也是变量逃逸的一种场景。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func create() (fs [2]func()){
    for i := 0; i < 2; i++ {
        // struct { F uintptr; i *int }
        fs[i] = func(){
            fmt.Println(&i, i)
        }    
    }
    return
}

func main() {
    fs := create()
    for i := 0; i < len(fs); i++ {
        fs[i]()
    }
    
    // Output:
    // 0xc0000ac058 2
    // 0xc0000ac058 2
}

捕获局部变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

func main() {
    fs := create()
    for i := 0; i < len(fs); i++ {
        fs[i]()
    }
}

//go:noinline
func create() (fs [2]func()) {
    for i := 0; i < 2; i++ {
        // 堆分配 struct { F uintptr; i *int }
        fs[i] = func() {
            i++
        }
    }
    return
}
  1. main 汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
func main() {
  0x4551e0        493b6610            CMPQ 0x10(R14), SP
  0x4551e4        7675                JBE 0x45525b
  0x4551e6        4883ec30            SUBQ $0x30, SP
  0x4551ea        48896c2428          MOVQ BP, 0x28(SP)
  0x4551ef        488d6c2428          LEAQ 0x28(SP), BP
    fs := create()
  0x4551f4        e887000000          CALL main.create(SB)    # 调用main.create
  0x4551f9        0f100424            MOVUPS 0(SP), X0        # 将0(SP)后16B内容放入X0,这里的两行代码相当于fs赋值
  0x4551fd        0f11442418          MOVUPS X0, 0x18(SP)     # 将X0迁移到0x18(SP)后16B
    for i := 0; i < len(fs); i++ {
  0x455202        48c744241000000000  MOVQ $0x0, 0x10(SP)     # i初始化 0
  0x45520b        eb00                JMP 0x45520d
  0x45520d        48837c241002        CMPQ $0x2, 0x10(SP)     # i和0x2比较    <--- 循环判断条件
  0x455213        7c02                JL 0x455217
  0x455215        eb2f                JMP 0x455246
        fs[i]()
  0x455217        488b442410          MOVQ 0x10(SP), AX       # AX=0
  0x45521c        0f1f4000            NOPL 0(AX)
  0x455220        4883f802            CMPQ $0x2, AX
  0x455224        7202                JB 0x455228
  0x455226        eb28                JMP 0x455250
  0x455228        488d44c418          LEAQ 0x18(SP)(AX*8), AX # AX=&funcval
  0x45522d        488b10              MOVQ 0(AX), DX          # DX=&funcval.fn
  0x455230        488b02              MOVQ 0(DX), AX          # AX=funcval.fn
  0x455233        ffd0                CALL AX                 # 调用函数
  0x455235        eb00                JMP 0x455237
    for i := 0; i < len(fs); i++ {
  0x455237        488b5c2410          MOVQ 0x10(SP), BX       # BX=0
  0x45523c        48ffc3              INCQ BX                 # BX=1
  0x45523f        48895c2410          MOVQ BX, 0x10(SP)       # i=1
  0x455244        ebc7                JMP 0x45520d            # <--- 一轮循环结束跳转循环开头
}
  0x455246        488b6c2428          MOVQ 0x28(SP), BP
  0x45524b        4883c430            ADDQ $0x30, SP
  0x45524f        c3                  RET
        fs[i]()
  0x455250        b902000000          MOVL $0x2, CX
  0x455255        e866d4ffff          CALL runtime.panicIndex(SB)
  0x45525a        90                  NOPL
func main() {
  0x45525b        0f1f440000          NOPL 0(AX)(AX*1)
  0x455260        e8fbccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x455265        e976ffffff          JMP main.main(SB)
  1. create 汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func create() (fs [2]func()) {
  0x455280        493b6610            CMPQ 0x10(R14), SP
  0x455284        0f86d0000000        JBE 0x45535a
  0x45528a        4883ec28            SUBQ $0x28, SP
  0x45528e        48896c2420          MOVQ BP, 0x20(SP)
  0x455293        488d6c2420          LEAQ 0x20(SP), BP
  0x455298        440f117c2430        MOVUPS X15, 0x30(SP)        # 0x30(SP)开始的16B地址清零
    for i := 0; i < 2; i++ {
  0x45529e        488d059b4a0000      LEAQ 0x4a9b(IP), AX         # AX=0x4a9b(IP),int的元类型    <--- 变量i初始化
  0x4552a5        e8965cfbff          CALL runtime.newobject(SB)  # 变量i申请内存,AX返回 *int
  0x4552aa        4889442418          MOVQ AX, 0x18(SP)
  0x4552af        48c70000000000      MOVQ $0x0, 0(AX)            # i变量赋值0
  0x4552b6        eb00                JMP 0x4552b8
  0x4552b8        488b4c2418          MOVQ 0x18(SP), CX           # CX=&i <--- 循环从这里开始,后面满足条件会跳转回来
  0x4552bd        48833902            CMPQ $0x2, 0(CX)            # i与2比较
  0x4552c1        7c05                JL 0x4552c8
  0x4552c3        e97d000000          JMP 0x455345
        fs[i] = func() {
        # struct { F uintptr; i *int }
  0x4552c8        488d0511740000      LEAQ 0x7411(IP), AX         # AX=0x7411(IP),funcval元类型 内存16B
  0x4552cf        e86c5cfbff          CALL runtime.newobject(SB)  # 申请内存16B,AX=&funcval
  0x4552d4        4889442410          MOVQ AX, 0x10(SP)
  0x4552d9        488d0da0000000      LEAQ main.create.func1(SB), CX  # CX=main.create.func1
  0x4552e0        488908              MOVQ CX, 0(AX)              # funcval.fn=main.create.func1
  0x4552e3        488b4c2410          MOVQ 0x10(SP), CX           # CX=&funcval
  0x4552e8        8401                TESTB AL, 0(CX)
  0x4552ea        488b542418          MOVQ 0x18(SP), DX           # DX=&int
  0x4552ef        488d7908            LEAQ 0x8(CX), DI            # DI=funcval.data
  0x4552f3        833d661f090000      CMPL $0x0, runtime.writeBarrier(SB) # 检查写屏障
  0x4552fa        7402                JE 0x4552fe
  0x4552fc        eb06                JMP 0x455304
  0x4552fe        48895108            MOVQ DX, 0x8(CX)            # funcval.data=&int
  0x455302        eb07                JMP 0x45530b
  0x455304        e837d0ffff          CALL runtime.gcWriteBarrierDX(SB) # 调用写屏障函数
  0x455309        eb00                JMP 0x45530b
  0x45530b        488b542418          MOVQ 0x18(SP), DX           # DX=&int
  0x455310        488b02              MOVQ 0(DX), AX              # AX=0
  0x455313        488b542410          MOVQ 0x10(SP), DX           # DX=&funcval
  0x455318        4883f802            CMPQ $0x2, AX               # AX和2比较
  0x45531c        7204                JB 0x455322
  0x45531e        6690                NOPW
  0x455320        eb2d                JMP 0x45534f
  0x455322        488d4cc430          LEAQ 0x30(SP)(AX*8), CX     # CX=0x30(SP)(AX*8),AX=0 -> 0x30(SP),AX=1 -> 0x38(SP)
  0x455327        488911              MOVQ DX, 0(CX)              # 返回数组遍历赋值
  0x45532a        eb00                JMP 0x45532c
    for i := 0; i < 2; i++ {
  0x45532c        488b4c2418          MOVQ 0x18(SP), CX           # CX=&int
  0x455331        488b09              MOVQ 0(CX), CX              # CX=0
  0x455334        488b542418          MOVQ 0x18(SP), DX           # DX=&int
  0x455339        48ffc1              INCQ CX                     # CX=1
  0x45533c        48890a              MOVQ CX, 0(DX)              # i赋值1
  0x45533f        90                  NOPL
  0x455340        e973ffffff          JMP 0x4552b8                # 跳转到前面开始下一轮循环
    return
  0x455345        488b6c2420          MOVQ 0x20(SP), BP
  0x45534a        4883c428            ADDQ $0x28, SP
  0x45534e        c3                  RET
        fs[i] = func() {
  0x45534f        b902000000          MOVL $0x2, CX
  0x455354        e867d3ffff          CALL runtime.panicIndex(SB)
  0x455359        90                  NOPL
func create() (fs [2]func()) {
  0x45535a        e801ccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x45535f        90                  NOPL
  0x455360        e91bffffff          JMP main.create(SB)
  1. main和create的栈布局。
          |   runtime.main callback
          ---------------------------
  +58 +28 |   runtime.main BP
          --------------------------- BP      ---------------
  +50 +20 |   &funcval            c.r
          ---------------------------
  +48 +18 |   &funcval            c.r
          ---------------------------
  +40 +10 |   0x0                 i           main.main栈
          ---------------------------
  +38 +08 |   &funcval            fs[1]
          ---------------------------
  +30 +00 |   &funcval            fs[0]
          --------------------------- SP      ---------------
  +28     |   main.main callback
          ---------------------------
  +20     |   main.main BP
          --------------------------- BP      ---------------
  +18     |   *int    -> 0 1      i
          ---------------------------
  +10     |   &funcval
          ---------------------------         main.create栈
  +08     |
          ---------------------------
  +00     |
          --------------------------- SP      ---------------

捕获参数

  1. 如果是参数被捕获,那么调用者依然从栈上传递参数,但是被调用函数会把它拷贝到堆上一份,然后和闭包函数都使用堆上分配的那一个。

  2. 值传递参数时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func Te1(a int) func() int { // 此时的a参数在堆上分配
    // 返回闭包捕获了函数参数,参数a会被复制到堆上供整个Te1函数及返回闭包使用
    // 堆分配 struct { F uintptr; a *int }
    return func() int {
        a++
        return a
    }
}

func main() {
    var a1 int
    f := Te1(a1)
    fmt.Println(f()) // 1
    fmt.Println(a1)  // 0
    
    // 可以看出 参数被闭包捕获关系的只是捕获函数内相关参数,参看下面汇编
}

值传递参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

func main() {
    a1 := 1
    b := Te1(a1)
    b()
}

//go:noinline
func Te1(a int) func() int {
    // 返回闭包捕获了函数参数
    // 参数a会被复制到堆上供整个Te1函数及返回闭包使用
    // struct { F uintptr; a *int }
    // 根据汇编可见,Te1堆分配了a,并把传入的参数1拷贝给了a。
    return func() int {
        a++
        return a
    }
}
  1. main 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
  0x4551e0        493b6610            CMPQ 0x10(R14), SP
  0x4551e4        7638                JBE 0x45521e
  0x4551e6        4883ec20            SUBQ $0x20, SP
  0x4551ea        48896c2418          MOVQ BP, 0x18(SP)
  0x4551ef        488d6c2418          LEAQ 0x18(SP), BP
    a1 := 1
  0x4551f4        48c744240801000000  MOVQ $0x1, 0x8(SP)      # a1=1
    b := Te1(a1)
  0x4551fd        b801000000          MOVL $0x1, AX           # AX=0x1
  0x455202        e839000000          CALL main.Te1(SB)       # 调用函数
  0x455207        4889442410          MOVQ AX, 0x10(SP)
    b()
  0x45520c        488b08              MOVQ 0(AX), CX          # CX=funcval.fn
  0x45520f        4889c2              MOVQ AX, DX             # DX=&funcval
  0x455212        ffd1                CALL CX
}
  0x455214        488b6c2418          MOVQ 0x18(SP), BP
  0x455219        4883c420            ADDQ $0x20, SP
  0x45521d        c3                  RET
func main() {
  0x45521e        6690                NOPW
  0x455220        e83bcdffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x455225        ebb9                JMP main.main(SB)
  1. main.Te1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func Te1(a int) func() int {
  0x455240        493b6610            CMPQ 0x10(R14), SP
  0x455244        0f8691000000        JBE 0x4552db
  0x45524a        4883ec30            SUBQ $0x30, SP
  0x45524e        48896c2428          MOVQ BP, 0x28(SP)
  0x455253        488d6c2428          LEAQ 0x28(SP), BP
  0x455258        4889442438          MOVQ AX, 0x38(SP)           # 参数a
  0x45525d        48c744241000000000  MOVQ $0x0, 0x10(SP)
  0x455266        488d05d34a0000      LEAQ 0x4ad3(IP), AX         # AX=0x4ad3(IP),int元类型,参数a堆分配
  0x45526d        e8ce5cfbff          CALL runtime.newobject(SB)  # 申请内存 AX=&int     *int
  0x455272        4889442420          MOVQ AX, 0x20(SP)           # 堆分配的变量a *int
  0x455277        488b4c2438          MOVQ 0x38(SP), CX           # CX=1
  0x45527c        488908              MOVQ CX, 0(AX)              # AX=&int -> 1
    return func() int {
    # struct { F uintptr; a *int }
  0x45527f        488d055a740000      LEAQ 0x745a(IP), AX         # AX=0x745a(IP),funcval结构体元类型,占16B
  0x455286        e8b55cfbff          CALL runtime.newobject(SB)  # 申请内存,AX=&funcval
  0x45528b        4889442418          MOVQ AX, 0x18(SP)
  0x455290        488d0d69000000      LEAQ main.Te1.func1(SB), CX # CX=main.Te1.func1
  0x455297        488908              MOVQ CX, 0(AX)              # funcval.fn=main.Te1.func1
  0x45529a        488b4c2418          MOVQ 0x18(SP), CX           # CX=&funcval
  0x45529f        8401                TESTB AL, 0(CX)
  0x4552a1        488b542420          MOVQ 0x20(SP), DX           # DX=&int
  0x4552a6        488d7908            LEAQ 0x8(CX), DI            # DI=funcval.data
  0x4552aa        833daf1f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x4552b1        7402                JE 0x4552b5
  0x4552b3        eb0b                JMP 0x4552c0
  0x4552b5        48895108            MOVQ DX, 0x8(CX)            # funcval.data=&int
  0x4552b9        eb0c                JMP 0x4552c7
  0x4552bb        0f1f440000          NOPL 0(AX)(AX*1)
  0x4552c0        e87bd0ffff          CALL runtime.gcWriteBarrierDX(SB)
  0x4552c5        eb00                JMP 0x4552c7
  0x4552c7        488b442418          MOVQ 0x18(SP), AX           # AX=&funcval
  0x4552cc        4889442410          MOVQ AX, 0x10(SP)
  0x4552d1        488b6c2428          MOVQ 0x28(SP), BP
  0x4552d6        4883c430            ADDQ $0x30, SP
  0x4552da        c3                  RET
func Te1(a int) func() int {
  0x4552db        4889442408          MOVQ AX, 0x8(SP)
  0x4552e0        e87bccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x4552e5        488b442408          MOVQ 0x8(SP), AX
  0x4552ea        e951ffffff          JMP main.Te1(SB)
  1. main.Te1.func1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
TEXT main.Te1.func1(SB) /mnt/hgfs/workspace/helium/main.go
  return func() int {
0x455300      4883ec18            SUBQ $0x18, SP        
0x455304      48896c2410          MOVQ BP, 0x10(SP)    
0x455309      488d6c2410          LEAQ 0x10(SP), BP    
0x45530e      488b4a08            MOVQ 0x8(DX), CX    
0x455312      48894c2408          MOVQ CX, 0x8(SP)    
0x455317      48c7042400000000    MOVQ $0x0, 0(SP)    
      a++
0x45531f      488b4c2408          MOVQ 0x8(SP), CX    
0x455324      488b09              MOVQ 0(CX), CX        
0x455327      488b542408          MOVQ 0x8(SP), DX    
0x45532c      48ffc1              INCQ CX            
0x45532f      48890a              MOVQ CX, 0(DX)        
      return a
0x455332      488b4c2408          MOVQ 0x8(SP), CX    
0x455337      488b01              MOVQ 0(CX), AX        
0x45533a      48890424            MOVQ AX, 0(SP)        
0x45533e      488b6c2410          MOVQ 0x10(SP), BP    
0x455343      4883c418            ADDQ $0x18, SP        
0x455347      c3                  RET
  1. 栈布局信息。
  +58 +20 |   runtime.main callback
          ----------------------------------
  +50 +18 |   runtime.main BP
          ----------------------------------  BP      --------------
  +48 +10 |   &funcval                变量b
          ----------------------------------
  +40 +08 |   0x1                     变量a1           main.main栈
          ----------------------------------
  +38 +00 |   0x1                     参数a
          ----------------------------------  SP      --------------
  +30     |   main.main callback
          ----------------------------------
  +28     |   main.main BP
          ----------------------------------  BP      --------------
  +20     |   &int    -> 1            a
          ----------------------------------
  +18     |   &funcval                r.o
          ----------------------------------
  +10        |   &funcval                r                main.Te1栈
          ----------------------------------
  +08     |
          ----------------------------------
  +00     |
          ----------------------------------  SP      --------------

  1. 引用传递参数时。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 如果捕获的是指针情况呢,根据下面代码可以看出跟上面情况一样的规则
// 这种情况跟捕获全局变量基本表现一致
func Te1(a *int) func() int {
    // 返回闭包捕获了函数参数,参数a会被复制到栈上供整个Te1函数及返回闭包使用
    // 堆分配 struct { F uintptr; a *int }
    return func() int {
        *a++
        return *a
    }
}

func main() {
    var a1 int
    f := Te1(&a1)
    fmt.Println(a1)  // 0
    fmt.Println(f()) // 1
    fmt.Println(a1)  // 1
    a1++
    fmt.Println(f()) // 3
    fmt.Println(a1)  // 3
    // 注意这里下面的一行代码,会先执行f() f() 在打印所以出现了奇怪的输出
    // 因为参数会先被实时计算,因此先执行了f()和f(),然后再打印数据的。
    // fmt.Println(a1, f(), a1, f(), a1) // 2 1 2 2 2
}

引用传递参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

func main() {
    a1 := 1
    b := Te1(&a1)
    b()
}

//go:noinline
func Te1(a *int) func() int {
    // 返回闭包捕获了函数参数
    // 参数a会被复制到堆上供整个Te1函数及返回闭包使用
    // struct { F uintptr; a *int }
    return func() int {
        *a++
        return *a
    }
}
  1. main 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
  0x4551e0      493b6610              CMPQ 0x10(R14), SP
  0x4551e4      764c                  JBE 0x455232
  0x4551e6      4883ec28              SUBQ $0x28, SP
  0x4551ea      48896c2420            MOVQ BP, 0x20(SP)
  0x4551ef      488d6c2420            LEAQ 0x20(SP), BP
    a1 := 1
    # 这里可以看出,直接就给a堆分配了
  0x4551f4        488d05454b0000      LEAQ 0x4b45(IP), AX         # AX=0x4b45(IP),int元类型
  0x4551fb        0f1f440000          NOPL 0(AX)(AX*1)
  0x455200        e83b5dfbff          CALL runtime.newobject(SB)  # AX=&int
  0x455205        4889442418          MOVQ AX, 0x18(SP)
  0x45520a        48c70001000000      MOVQ $0x1, 0(AX)            # a=1
    b := Te1(&a1)
  0x455211        488b442418          MOVQ 0x18(SP), AX           # AX=&int
  0x455216        e825000000          CALL main.Te1(SB)           # 调用函数,AX=&funcval
  0x45521b        4889442410          MOVQ AX, 0x10(SP)
    b()
  0x455220        488b08              MOVQ 0(AX), CX              # CX=funcval.fn
  0x455223        4889c2              MOVQ AX, DX                 # DX=&funcval
  0x455226        ffd1                CALL CX                     # 调用函数
}
  0x455228        488b6c2420          MOVQ 0x20(SP), BP
  0x45522d        4883c428            ADDQ $0x28, SP
  0x455231        c3                  RET
func main() {
  0x455232        e829cdffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x455237        eba7                JMP main.main(SB)
  1. main.Te1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func Te1(a *int) func() int {
 0x455240     493b6610            CMPQ 0x10(R14), SP
 0x455244     7675                JBE 0x4552bb
 0x455246     4883ec28            SUBQ $0x28, SP
 0x45524a     48896c2420          MOVQ BP, 0x20(SP)
 0x45524f     488d6c2420          LEAQ 0x20(SP), BP
 0x455254     4889442430          MOVQ AX, 0x30(SP)       # 参数a *int
 0x455259     48c744241000000000  MOVQ $0x0, 0x10(SP)     # 返回值 func() int
   return func() int {
   # struct { F uintptr; i *int }
 0x455262     488d0577740000      LEAQ 0x7477(IP), AX     # AX=0x7477(IP),funcval结构体 16B
 0x455269     e8d25cfbff          CALL runtime.newobject(SB   # AX=&funcval
 0x45526e     4889442418          MOVQ AX, 0x18(SP)
 0x455273     488d0d66000000      LEAQ main.Te1.func1(SB), CX # CX=main.Te1.func1
 0x45527a     488908              MOVQ CX, 0(AX)              # funcval.fn=main.Te1.func1
 0x45527d     488b4c2418          MOVQ 0x18(SP), CX           # CX=&funcval
 0x455282     8401                TESTB AL, 0(CX)
 0x455284     488b542430          MOVQ 0x30(SP), DX           # DX=&int
 0x455289     488d7908            LEAQ 0x8(CX), DI            # DI=funcval.data
 0x45528d     833dcc1f090000      CMPL $0x0, runtime.writeBarrier(SB)
 0x455294     7402                JE 0x455298
 0x455296     eb08                JMP 0x4552a0
 0x455298     48895108            MOVQ DX, 0x8(CX)            # funcval.data=&int
 0x45529c     eb09                JMP 0x4552a7
 0x45529e     6690                NOPW
 0x4552a0     e89bd0ffff          CALL runtime.gcWriteBarrierDX(SB)
 0x4552a5     eb00                JMP 0x4552a7
 0x4552a7     488b442418          MOVQ 0x18(SP), AX           # AX=&funcval
 0x4552ac     4889442410          MOVQ AX, 0x10(SP)
 0x4552b1     488b6c2420          MOVQ 0x20(SP), BP
 0x4552b6     4883c428            ADDQ $0x28, SP
 0x4552ba     c3                  RET
  1. main.Te1.func1 函数汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Te1(a *int) func() int {
 0x4552bb     4889442408          MOVQ AX, 0x8(SP)
 0x4552c0     e89bccffff          CALL runtime.morestack_noctxt.abi0(SB)
 0x4552c5     488b442408          MOVQ 0x8(SP), AX
 0x4552ca     e971ffffff          JMP main.Te1(SB)
return func() int {
 0x4552e0     4883ec18            SUBQ $0x18, SP
 0x4552e4     48896c2410          MOVQ BP, 0x10(SP)
 0x4552e9     488d6c2410          LEAQ 0x10(SP), BP
 0x4552ee     488b4a08            MOVQ 0x8(DX), CX    # CX=&int
 0x4552f2     48894c2408          MOVQ CX, 0x8(SP)
 0x4552f7     48c7042400000000    MOVQ $0x0, 0(SP)
       *a++
 0x4552ff     488b4c2408          MOVQ 0x8(SP), CX    # CX=&int
 0x455304     8401                TESTB AL, 0(CX)
 0x455306     488b542408          MOVQ 0x8(SP), DX    # DX=&int
 0x45530b     8402                TESTB AL, 0(DX)
 0x45530d     488b09              MOVQ 0(CX), CX      # CX=1
 0x455310     48ffc1              INCQ CX             # CX=2
 0x455313     48890a              MOVQ CX, 0(DX)      # &int -> 2
       return *a
 0x455316     488b4c2408          MOVQ 0x8(SP), CX    # CX=&int
 0x45531b     8401                TESTB AL, 0(CX)
 0x45531d     488b01              MOVQ 0(CX), AX      # AX=2
 0x455320     48890424            MOVQ AX, 0(SP)
 0x455324     488b6c2410          MOVQ 0x10(SP), BP
 0x455329     4883c418            ADDQ $0x18, SP
 0x45532d     c3                  RET
  1. 栈分布情况。
  +58 +28 |   address of runtime.main
          ------------------------------
  +50 +20 |   BP of runtime.main
          ------------------------------  BP
  +48 +18 |   &int    -> 1            a1
          ------------------------------
  +40 +10 |   &funcval                b
          ------------------------------
  +38 +08 |
          ------------------------------
  +30 +00 |   &int                    T.a
          ------------------------------  SP
  +28     |   address of main.main
          ------------------------------
  +20     |   BP of main.main
          ------------------------------  BP
  +18     |   &funcval                T.o
          ------------------------------
  +10     |   &funcval                T.r
          ------------------------------
  +08     |
          ------------------------------
  +00     |
          ------------------------------  SP

捕获返回值

  1. 如果是返回值被捕获,那么处理方式就又有些不同了。
  2. 返回值空间依然由调用者在栈上分配,但是被调用函数(闭包的外层函数)会在堆上也分配一个,并且与闭包函数都使用堆上这一个,但是,在外层函数返回前要把堆上的返回值拷贝到栈上那一个。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

func main() {
    b, _ := Te1()
    b()
}

//go:noinline
func Te1() (y func() int, y1 int) {
    y1++ // 1
    // 堆分配 struct { F uintptr; y1 *int }
    return func() int {
        y1++
        return 1
    }, y1
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
  0x4551e0      493b6610            CMPQ 0x10(R14), SP
  0x4551e4      763b                JBE 0x455221
  0x4551e6      4883ec18            SUBQ $0x18, SP
  0x4551ea      48896c2410          MOVQ BP, 0x10(SP)
  0x4551ef      488d6c2410          LEAQ 0x10(SP), BP
    b, _ := Te1()
  0x4551f4      48c744240800000000  MOVQ $0x0, 0x8(SP)
  0x4551fd      0f1f00              NOPL 0(AX)
  0x455200      e83b000000          CALL main.Te1(SB)
  0x455205      4889442408          MOVQ AX, 0x8(SP)
  0x45520a      48890424            MOVQ AX, 0(SP)
    b()
  0x45520e      488b1424            MOVQ 0(SP), DX
  0x455212      488b02              MOVQ 0(DX), AX
  0x455215      ffd0                CALL AX
}
  0x455217      488b6c2410          MOVQ 0x10(SP), BP
  0x45521c      4883c418            ADDQ $0x18, SP
  0x455220      c3                  RET
func main() {
  0x455221      e83acdffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x455226      ebb8                JMP main.main(SB)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func Te1() (y func() int, y1 int) {
  0x455240      493b6610            CMPQ 0x10(R14), SP
  0x455244      0f868e000000        JBE 0x4552d8
  0x45524a      4883ec30            SUBQ $0x30, SP
  0x45524e      48896c2428          MOVQ BP, 0x28(SP)
  0x455253      488d6c2428          LEAQ 0x28(SP), BP
  0x455258      48c744241000000000  MOVQ $0x0, 0x10(SP)
  0x455261      488d05d84a0000      LEAQ 0x4ad8(IP), AX         # AX=0x4ad8(IP),int元类型
  0x455268      e8d35cfbff          CALL runtime.newobject(SB)  # AX=&int
  0x45526d      4889442420          MOVQ AX, 0x20(SP)
    y1++
  0x455272      48ff00              INCQ 0(AX)                  # y1=1
    return func() int {
    # struct { F uintptr; y1 *int }
  0x455275      488d0564740000      LEAQ 0x7464(IP), AX         # AX=0x7464(IP),funcval元类型 16B
  0x45527c      0f1f4000            NOPL 0(AX)
  0x455280      e8bb5cfbff          CALL runtime.newobject(SB)  # AX=&funcval
  0x455285      4889442418          MOVQ AX, 0x18(SP)
  0x45528a      488d0d6f000000      LEAQ main.Te1.func1(SB), CX # CX=main.Te1.func1
  0x455291      488908              MOVQ CX, 0(AX)              # funcval.fn=main.Te1.func1
  0x455294      488b4c2418          MOVQ 0x18(SP), CX           # CX=&funcval
  0x455299      8401                TESTB AL, 0(CX)
  0x45529b      488b542420          MOVQ 0x20(SP), DX           # DX=&int
  0x4552a0      488d7908            LEAQ 0x8(CX), DI            # DI=funcval.data
  0x4552a4      833db51f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x4552ab      7402                JE 0x4552af
  0x4552ad      eb06                JMP 0x4552b5
  0x4552af      48895108            MOVQ DX, 0x8(CX)            # funcval.data=&int
  0x4552b3      eb07                JMP 0x4552bc
  0x4552b5      e886d0ffff          CALL runtime.gcWriteBarrierDX(SB)
  0x4552ba      eb00                JMP 0x4552bc
  0x4552bc      488b442418          MOVQ 0x18(SP), AX           # AX=&funcval  返回参数1
  0x4552c1      4889442410          MOVQ AX, 0x10(SP)
  0x4552c6      488b4c2420          MOVQ 0x20(SP), CX           # CX=&int
  0x4552cb      488b19              MOVQ 0(CX), BX              # BX=1         返回参数2
  0x4552ce      488b6c2428          MOVQ 0x28(SP), BP
  0x4552d3      4883c430            ADDQ $0x30, SP
  0x4552d7      c3                  RET
func Te1() (y func() int, y1 int) {
  0x4552d8      e883ccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x4552dd      0f1f00              NOPL 0(AX)
  0x4552e0      e95bffffff          JMP main.Te1(SB)
    +50 +18 |   address of runtime.main
            -----------------------------
    +48 +10 |   BP of runtime.main
            -----------------------------   BP
    +40 +08 |   0
            -----------------------------
    +38 +00 |
            -----------------------------   SP
    +30     |   address of main.main
            -----------------------------
    +28     |   BP of main.main
            -----------------------------   BP
    +20     |   &int    1           y1
            -----------------------------
    +18     |   &funcval            y
            -----------------------------
    +10     |   &funcval
            -----------------------------
    +08     |
            -----------------------------
    +00     |
            -----------------------------   SP

自己捕获自己
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

func main() {
    x()()
}

//go:noinline
func x() (y func()) {
    // struct { F uintptr }
    y = func() {
        //println("y")
    }

    // defer在return赋值完成之后执行
    //defer func() {
    //	y = func() {
    //        fmt.Println("我能改变了Y的值?")
    //	}
    //}()

    // struct { F uintptr; y *func() }
    return func() {
        //println("z")
        y()
    }
}
  1. main 汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
  0x4551e0        493b6610        CMPQ 0x10(R14), SP
  0x4551e4        7629            JBE 0x45520f
  0x4551e6        4883ec10        SUBQ $0x10, SP
  0x4551ea        48896c2408      MOVQ BP, 0x8(SP)
  0x4551ef        488d6c2408      LEAQ 0x8(SP), BP
    x()()
  0x4551f4        e847000000      CALL main.x(SB)     # AX=&funcval
  0x4551f9        48890424        MOVQ AX, 0(SP)
  0x4551fd        488b08          MOVQ 0(AX), CX      # CX=funcval.fn
  0x455200        4889c2          MOVQ AX, DX        	# DX=&funcval
  0x455203        ffd1            CALL CX
}
  0x455205        488b6c2408      MOVQ 0x8(SP), BP
  0x45520a        4883c410        ADDQ $0x10, SP
  0x45520e        c3              RET
func main() {
  0x45520f        e84ccdffff      CALL runtime.morestack_noctxt.abi0(SB)
  0x455214        ebca        	JMP main.main(SB)
  1. x 汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
func x() (y func()) {
  0x455240        493b6610            CMPQ 0x10(R14), SP
  0x455244        0f86bf000000        JBE 0x455309
  0x45524a        4883ec28            SUBQ $0x28, SP
  0x45524e        48896c2420          MOVQ BP, 0x20(SP)
  0x455253        488d6c2420          LEAQ 0x20(SP), BP
  # func()    变量y
  0x455258        488d05e1480000      LEAQ 0x48e1(IP), AX               # AX=0x48e1(IP),funcval元类型
  0x45525f        90                  NOPL
  0x455260        e8db5cfbff          CALL runtime.newobject(SB)        # AX=&funcval
  0x455265        4889442418          MOVQ AX, 0x18(SP)
    y = func() {
  0x45526a        833def1f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x455271        7402                JE 0x455275
  0x455273        eb0d                JMP 0x455282
  # funcval.fn -> println("y") 替换 y
  0x455275        488d0d841d0100      LEAQ 0x11d84(IP), CX   # CX=0x11d84(IP),函数代码处
  0x45527c        488908              MOVQ CX, 0(AX)         # funcval.fn=0x11d84(IP)
  0x45527f        90                  NOPL
  0x455280        eb11                JMP 0x455293
  0x455282        4889c7              MOVQ AX, DI
  0x455285        488d0d741d0100      LEAQ 0x11d74(IP), CX
  0x45528c        e88fd0ffff          CALL runtime.gcWriteBarrierCX(SB)
  0x455291        eb00                JMP 0x455293
    return func() {
    # struct { F uintptr; y *func() }
  0x455293        488d0586740000      LEAQ 0x7486(IP), AX          # AX=0x7486(IP),funcval元类型 16B
  0x45529a        e8a15cfbff          CALL runtime.newobject(SB)   # AX=&funcval.1
  0x45529f        4889442410          MOVQ AX, 0x10(SP)
  0x4552a4        488d0d75000000      LEAQ main.x.func2(SB), CX    # CX=main.x.func2
  0x4552ab        488908              MOVQ CX, 0(AX)               # funcval.1.fn=main.x.func2
  0x4552ae        488b4c2410          MOVQ 0x10(SP), CX            # CX=&funcval.1
  0x4552b3        8401                TESTB AL, 0(CX)
  0x4552b5        488b542418          MOVQ 0x18(SP), DX            # DX=&funcval
  0x4552ba        488d7908            LEAQ 0x8(CX), DI             # DI=funcval.1.data
  0x4552be        833d9b1f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x4552c5        7402                JE 0x4552c9
  0x4552c7        eb06                JMP 0x4552cf
  0x4552c9        48895108            MOVQ DX, 0x8(CX)             # funcval.1.data=&funcval
  0x4552cd        eb07                JMP 0x4552d6
  0x4552cf        e86cd0ffff          CALL runtime.gcWriteBarrierDX(SB)
  0x4552d4        eb00                JMP 0x4552d6
  0x4552d6        488b7c2418          MOVQ 0x18(SP), DI            # DI=&funcval
  0x4552db        488b4c2410          MOVQ 0x10(SP), CX            # CX=&funcval.1
  0x4552e0        833d791f090000      CMPL $0x0, runtime.writeBarrier(SB)
  0x4552e7        7402                JE 0x4552eb
  0x4552e9        eb05                JMP 0x4552f0
  0x4552eb        48890f              MOVQ CX, 0(DI)               # funcval.fn=&funcval.1
  0x4552ee        eb07                JMP 0x4552f7
  0x4552f0        e82bd0ffff          CALL runtime.gcWriteBarrierCX(SB)
  0x4552f5        eb00                JMP 0x4552f7
  0x4552f7        488b4c2418          MOVQ 0x18(SP), CX            # CX=&funcval
  0x4552fc        488b01              MOVQ 0(CX), AX               # AX=funcval.fn
  0x4552ff        488b6c2420          MOVQ 0x20(SP), BP
  0x455304        4883c428            ADDQ $0x28, SP
  0x455308        c3                  RET
func x() (y func()) {
  0x455309        e852ccffff          CALL runtime.morestack_noctxt.abi0(SB)
  0x45530e        e92dffffff          JMP main.x(SB)

unsafe

  1. Go语言里Function Value本质上是指向funcval结构体的指针
  2. Go语言里闭包只是拥有捕获列表的Function Value
  3. 捕获变量在外层函数与闭包函数中要保持一致
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "fmt"
    "unsafe"
)

type fv struct {
    fn unsafe.Pointer
    i *int  // 捕获的变量i
}

func main() {
    g := callback()

    fn := **(**fv)(unsafe.Pointer(&g))

    fmt.Printf("%#v\n", fn)

    fmt.Println(*(fn.i)) // 13
    
    // Output:
    // main.fv{fn:(unsafe.Pointer)(0xeccdc0), i:(*int)(0xc00000a098)}
    // 13
    
    // 可以看出捕获的是i的地址
}

func callback() func() {
    i := 13
    // struct { F uintptr; i *int }
    return func() {
        i++
    }
}

参考

  1. 本篇文章参考了《深度探索Go语言》书内容。
  2. 参考 https://mp.weixin.qq.com/s/hO0S4WcG0hUzCmMNMKtS-g