深入理解 Go | defer

以下基于 Go 1.14

Go 语言中的 defer 常用来进行资源释放。它有以下几个特点:

  • defer 传入的函数会在当前函数或者方法返回之前运行。
  • 函数中调用的多个 defer 会以先调用后执行的方式进行
  • 在调用 defer 时,就会对函数传入的参数进行计算。

defer 类型

有三种类型的 defer

编译器的 ssa 过程中会确认当前 defer 的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// compile.internal.gc.state.stmt
func (s *state) stmt(n *Node) {
//...
switch n.Op {
// ...
case ODEFER:
// ...
if s.hasOpenDefers {
s.openDeferRecord(n.Left)
} else {
d := callDefer
if n.Esc == EscNever {
d = callDeferStack
}
s.call(n.Left, d)
}
}
// ...
}

可以使用 $ go tool compile -d defer hello.go 来检查 defer 类型。

open-coded

Go 1.14 引入,目的是优化 defer 的运行时间。编译器在 ssa 过程中,会将被延迟的方法直接插入到函数的尾部(inline),从而避免运行时的 deferprocdeferprocStack 操作,以及多次调用 deferreturn

  • 以下情况不使用这种类型来处理 defer
    • 函数中对 defer 的调用次数超过 8(这是为了最小化代码大小,只使用 1 个 byte 来辅助标识)(例如下面的 f0f1
    • 函数中存在出现在循环中的 defer(例如下面的 f2f3f4f5
      • 包括使用 for 构造的和使用 label+goto 构造的
    • 函数中出现过多(返回语句次数 * defer 个数 > 15)的返回语句
      • 因为会在每个返回点前插入被 defer 的函数调用
    • gcflags 无 N

举例说明:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 8 次 defer
func f0() {
defer func() { // open-coded defer
fmt.Println("defer0")
}()
defer func() { // open-coded defer
fmt.Println("defe1")
}()
defer func() { // open-coded defer
fmt.Println("defer2")
}()
defer func() { // open-coded defer
fmt.Println("defe3")
}()
defer func() { // open-coded defer
fmt.Println("defer4")
}()
defer func() { // open-coded defer
fmt.Println("defe5")
}()
defer func() { // open-coded defer
fmt.Println("defer6")
}()
defer func() { // open-coded defer
fmt.Println("defer7")
}()
fmt.Println("f0")
}

func f1() { // 9 次 defer
defer func() { // stack-allocated defer
fmt.Println("defer0")
}()
defer func() { // stack-allocated defer
fmt.Println("defe1")
}()
defer func() { // stack-allocated defer
fmt.Println("defer2")
}()
defer func() { // stack-allocated defer
fmt.Println("defe3")
}()
defer func() { // stack-allocated defer
fmt.Println("defer4")
}()
defer func() { // stack-allocated defer
fmt.Println("defe5")
}()
defer func() { // stack-allocated defer
fmt.Println("defer6")
}()
defer func() { // stack-allocated defer
fmt.Println("defer7")
}()
defer func() { // stack-allocated defer
fmt.Println("defer8")
}()
fmt.Println("f1")
}

func f2() { // defer 没有出现在循环中(for)
defer func() { // open-coded defer
fmt.Println("defer0")
}()
for i := 0; i < 1; i += 1 {
fmt.Println("f2", i)
}
}

func f3() { // defer 出现在循环中(for)
for i := 0; i < 1; i += 1 {
defer func() { // heap-allocated defer
fmt.Println("defer0")
}()
}
fmt.Println("f3")
}

func f4() { // defer 没有出现在循环中(label+goto)
defer func() { // open-coded defer
fmt.Println("defer0")
}()
label:
fmt.Println("f4")
goto label
}

func f5() { // defer 出现在循环中(label+goto)
label:
defer func() { // heap-allocated defer
fmt.Println("defer0")
}()
fmt.Println("f5")
goto label
}

stack-allocated

Go 1.13 引入,用于优化性能,表示在栈上分配 defer 相关的结构体

那么,什么时候会在栈上分配呢?答案在下面这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/cmd/compile/internal/gc/escape.go
func (e *Escape) augmentParamHole(k EscHole, call, where *Node) EscHole {
// ...
// Top level defers arguments don't escape to heap, but they
// do need to last until end of function. Tee with a
// non-transient location to avoid arguments from being
// transiently allocated.
if where.Op == ODEFER && e.loopDepth == 1 {
// force stack allocation of defer record, unless open-coded
// defers are used (see ssa.go)
where.Esc = EscNever
return e.later(k)
}
// ...
}

举例说明:

1
2
3
4
5
6
7
8
9
10
11
func f6() {
defer func() { // stack-allocated defer
fmt.Println("defer2")
}()
for {
defer func() { // heap-allocated defer
fmt.Println("defer1")
}()
break
}
}

heap-allocated

表示在堆上分配 defer 相关的结构体,最原始的方式。

实现原理

一个数据结构

在 Go 中,defer 关键字对应的数据结构为 runtime._defer。这是一个用链表实现的栈。

编译时

  • 处理 defer 关键字

    • 如果是 open-coded 类型的 defer,则调用 cmd/compile/internal/gc.state.openDeferRecord 方法,
    • 如果是 stack-allocated 类型,则转换成 runtime.deferprocStack
    • 如果是 heap-allocated 类型,则转换成 runtime.deferproc
  • 在调用 defer 的函数返回之前插入 runtime.deferreturn

    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
    // compile.internal.gc.state
    // 处理任何需要在返回前生成的代码
    func (s *state) exit() *ssa.Block {
    if s.hasdefer { // 函数中存在 defer 调用
    if s.hasOpenDefers { // 如果有 open-coded 类型的 defer
    if shareDeferExits && s.lastDeferExit != nil && len(s.openDefers) == s.lastDeferCount {
    if s.curBlock.Kind != ssa.BlockPlain {
    panic("Block for an exit should be BlockPlain")
    }
    s.curBlock.AddEdgeTo(s.lastDeferExit)
    s.endBlock()
    return s.lastDeferFinalBlock
    }
    s.openDeferExit()
    } else { // 对于其他类型的 defer,调用
    s.rtcall(Deferreturn, true, nil)
    }
    }
    //...
    }

    // openDeferExit 生成 SSA,从而在退出的时候处理所有的 open-coded 类型的 defer。
    // 这个过程会加载 deferBits 字段,然后检查这个字段的每个位,检查是否执行了对应的 defer 语句。
    // 对于每一个打开的位,会进行相关的 defer 调用。
    func (s *state) openDeferExit() {
    // ...
    }

运行时

  • 如果调用了 runtime.deferprocStack 或者 runtime.deferproc,那么它们都会将一个新的 runtime._defer 结构体(此时就会对函数参数进行计算)追加到当前 Goroutine 的 _defer 链表的头部
  • runtime.deferreturn 会从 Goroutine 的 _defer 链表中取出 runtime._defer 结构并执行
    • 如果是 open-coded 类型的延迟调用,则会调用 runtime.runOpenDeferFrame 函数来运行该 _defer 结构中所有有效的延迟调用。
    • 否则,它会调用 runtime·jmpdefer 函数。这个函数会跳到对应被延迟调用的函数并执行
    • 会多次调用 runtime.deferreturn,直到所有的延迟调用都执行完毕。

参考

请言小午吃个甜筒~~