go语言defer关键字浅析

Go语言 defer 关键字浅析

Go 语言中 defer 是比较常用的关键字,为了使用好他,我站在底层简单的剖析一下。

一、使用现象

1. defer 的执行时机

Go 官方对于 defer 的解释

1
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
  • 包含着 defer 的函数返回时
  • 包含着 defer 的函数执行到最后时
  • 当前的 goroutinue 发生 panic 时

2. defer、return、返回值的执行顺序

1
if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.
  • 先给返回值赋值
  • 执行 defer 语句
  • 函数返回

3. defer 函数参数传递

对于 defer 函数调用,如下,我们想统计函数的调用耗时,预期结果是打印 2 秒左右,但是实际输出 0 秒左右

1
2
3
4
5
6
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))

time.Sleep(2 * time.Second)
}

因为调用 defer 关键字会立刻复制函数中引用的外部参数,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的。

二、底层实现

本文粘贴的源码基于 Go 语言版本:1.15

1. 不同 Go语言版本 defer 的实现

  • Go 1.11 - 1.12 堆中分配

    • 编译器将 defer 关键字转换为 runtime.deferproc,并在调用 defer 关键字的函数返回之前插入 runtime.deferreturn
    • 运行时调用 runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutinue 的链表头
    • 运行时调用 runtime.deferreturn 会从 Goroutinue 的链表中取出 runtime._defer 结构并依次执行
  • Go 1.13 栈上分配

    将 defer 关键字在函数中最多执行一次时,编译期间会将 _defer 结构体分配到栈上,并调用 runtime.deferprocstack

  • Go 1.14 - 至今 开放编码

    • 编译期间判断 defer 关键字、return 语句的数目确定是否开启开放编码优化
    • 通过 deferBits 和 cmd/compile/internal/gc.openDeferInfo 存储 defer 关键字相关信息
    • 如果 defer 关键字的执行可以在编译期间确定,会在函数返回前直接插入相应代码,否则会由运行时的 runtime.deferreturn 处理

2. 基本 _defer 结构体

_defer 结构是 defer 关键字在 Go 语言源代码中对应的数据结构。在源码 go/src/runtime/runtime2.go

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
type _defer struct {
siz int32 // 参数和结果的内存大小
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
// 开放编码优化
openDefer bool
sp uintptr // 栈指针
pc uintptr // 调用方的程序计数器
fn *funcval // 是 defer 关键字中传入的函数
_panic *_panic // 触发延迟调用的结构体,可能为空
link *_defer // 指向下一个 _defer 结构体,是一个链表结构

// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}

runtime._defer 结构体是延迟调用链表上的一个元素,所有结构体都会通过 link 字段串联成链表。

Go 语言代码在编译时,中间代码生成阶段 cmd/compile/internal/gc/ssa.go 中 stmt 会负责处理程序中的 defer ,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *state) stmt(n *Node) {
...
case ODEFER:
...
// 开放编码如果开启,直接使用
if s.hasOpenDefers {
s.openDeferRecord(n.Left)
} else {
// 堆中分配
d := callDefer
if n.Esc == EscNever {
// 栈上分配
d = callDeferStack
}
s.call(n.Left, d)
}
...
}

堆中分配、栈上分配和开发编码是处理 defer 关键字的 3 种方法。堆中分配 _defer 结构体是兜底方案

三、 堆中分配

在堆上分配和在栈上分配 _defer 都会调用 cmd/compile/internal/gc/ssa.go 中 call 函数,表明此时 defer 在编译器看来也是函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deferproc = sysfunc("deferproc")

func (s *state) call(n *Node, k callKind) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
...
} else {
// call target
switch {
case k == callDefer:
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem())
...
}
call.AuxInt = stksize // Call operations carry the argsize of the callee along with them
}
s.vars[&memVar] = call
...
}

cmd/compile/internal/gc/ssa.go 中 call 函数会负责为所有函数调用生成中间代码,他的工作内容包括:

  1. 获取需要执行的函数名、闭包函数、代码指针和函数调用的接收方
  2. 获取栈地址并将函数的参数写入堆中
  3. 使用 cmd/compile/internal/gc/ssa.go 中的 newValue1A 函数生成函数调用的中间代码
  4. 如果当前调用的函数是 defer,那么将单独生成相关结束代码块
  5. 获取函数的返回值地址并结束当前调用

deferproc 是语言层面的 runtime.function,而 defer 关键字就转换成了 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
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
// cmd/compile/internal/gc/walk.go
func walkstmt(n *Node) *Node {
...
switch n.Op {
...
case ODEFER:
Curfn.Func.SetHasDefer(true) // 设置当前函数有 hasDefer 属性
Curfn.Func.numDefers++
if Curfn.Func.numDefers > maxOpenDefers {
// 如果函数中有超过8个 defer,不要允许开放编码的 defer,因为我们使用一个字节来记录活动的延迟。
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
if n.Esc != EscNever {
// 如果n.Esc不是EscNever,则此 defer 在循环中发生,因此开放编码的 defer 不能在此函数中使用
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
fallthrough
...
}
...
}

// cmd/compile/internal/gc/ssa.go
func buildssa(fn *Node, worker int) *ssa.Func {
...
var s state
...
s.hasdefer = fn.Func.HasDefer()
...
}

// cmd/compile/internal/gc/ssa.go
func (s *state) exit() *ssa.Block {
// 如果设置了 hasdefer
if s.hasdefer {
// 设置了开放编码
if s.hasOpenDefers {
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 {
// 在函数返回前插入 Deferreturn
s.rtcall(Deferreturn, true, nil)
}
}
...
}
  • 设置当前函数具有 hasDefer 属性。同时也设置了其他,比如函数中超过 8 个 defer 调用或者 defer 在循环中发生,则不开启开放编码功能
  • 然后在 buildssa 函数中设置 state 的 hasdefer 状态
  • 最后,在 exit 函数中根据 state 的 hasdefer 的状态,在函数返回前插入 Deferreturn 函数

预总结:当Go 语言的运行时将 runtime._defer 分配到堆中时,编译器将 defer 转换成 deferproc 函数,然后在调用 defer 的函数末尾插入 runtime.deferreturn 函数。而这两个函数的作用如下:

  • runtime.deferproc 负责创建新的延迟调用
  • runtime.deferreturn 负责在函数调用结束时执行所有延迟调用

1. deferproc 创建延迟调用

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
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}

// the arguments of fn are in a perilous state. The stack map
// for deferproc does not describe them. So we can't let garbage
// collection or stack copying trigger until we've copied them out
// to somewhere safe. The memmove below does that.
// Until the copy completes, we can only call nosplit routines.
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()

// 获得 runtime._defer 结构体
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}

// 通常返回 0,并且是一个保证不会触发 defer 调用的函数,可以避免 runtime.deferreturn 的递归调用
return0()
}

runtime.deferproc 会通过 newdefer 函数为 defer 创建一个新的 runtime._defer 结构体、设置他的函数指针 fn、程序计数器 pc、和栈指针 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
// 从调度器的延迟调用缓存池中取出结构体,并且将其追加到当前 Goroutinue 的缓存池中
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// Take the slow path on the system stack so
// we don't grow newdefer's stack.
systemstack(func() {
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil {
// Allocate new defer+args.
// 在堆上创建 _defer 结构体
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
if debugCachedWork {
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
}
d.siz = siz
d.heap = true
return d
}

runtime.newdefer 会获得 runtime._defer 的结构体,有三种方式

  • 从调度器的延迟调用缓存池 sched.deferpool 中取出结构体,并且将其追加到当前 Goroutinue 的缓存池中
  • 从 Gorontinue 的延迟调用缓存池 pp.deferpool 中取出结构体
  • 通过 runtime.mallocgc 在堆中创建一个 _defer 结构体

无论通过那种方式,只要获得了 runtime._defer 结构体,都会把他“头插”到所在的 Goroutinue _defer 链表中。

因此我们可以知道,defer 关键字是在链表中“头插”的,而执行是从前向后的,因此会导致后调用 defer 的先执行。

2. 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
28
29
30
31
32
33
34
35
36
37
38
// go/src/runtime/panic.go 
func deferreturn(arg0 uintptr) {
gp := getg()
// 从 Goroutinue 的 _defer 链表中取出最前面的 runtime._defer
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}

switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
// 如果 defer 函数指针是 nil,则强制段错误,而不是在 jmpdefer 中
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

runtime.deferreturn 会从 Goroutinue 的 _defer 链表中取出最前面的 runtime._defer ,并且调用 runtime.jmpdefer 传入需要执行的函数和参数。而 runtime.jmpdefer 是一个用汇编语言实现的运行时函数。其主要工作是跳转到 defer 所在的代码段并在执行结束之后跳转回 runtime.deferreturn

runtime.deferreturn 会执行完当前 Goroutinue 的 _defer 链表中的所有延迟函数

四、栈上分配

默认情况下,Go 语言中 runtime._defer 结构体都会在堆上分配,如果能够分配在栈上,就可以节约内存分配带来的性能开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// cmd/compile/internal/gc/ssa.go 
func (s *state) call(n *Node, k callKind) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上创建 _defer 结构体
t := deferstruct(stksize)
...
// Call runtime.deferprocStack with pointer to _defer record.
arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize())
s.store(types.Types[TUINTPTR], arg0, addr)
// 调用 deferprocStack
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem())
if stksize < int64(Widthptr) {
// We need room for both the call to deferprocStack and the call to
// the deferred function.
stksize = int64(Widthptr)
}
call.AuxInt = stksize
}
...
}

当 defer 关键字在函数体中最多执行一次时,编译期间的 call 函数会将结构体分配到栈上,并且调用 runtime.deferprocStack 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// go/src/runtime/panic.go 
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// 设置一些未在编译期间初始化的字段
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0

*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

// 同样的通常返回 0,并且这个函数保证不会调用 defer
return0()
}

因为在编译时已经创建了 runtime._defer 结构体,所以在运行期间 runtime.deferprocStack 只需要设置一些未在编译期间初始化的字段即可,然后就可以将 _defer 结构体加入到函数的链表上了。

除了分配位置不同,栈上分配和堆上分配的 runtime._defer 没有本质的不同,而在栈上分配可以在编译时建立好 _defer 对象,节省了堆上内存分配的消耗。条件是当 defer 关键字在函数体中最多执行一次时。该方法可以将defer 关键字的额外开销降低约 30%

五、开放编码

开放编码模式使用代码内联优化 defer 关键字的额外开销,并引入函数数据 funcdata 管理 panic 的调用,该优化可以将 defer 的调用开销从 Go 1.13 的约 35ns 降至约 6ns。

1
2
3
With normal (stack-allocated) defers only: 35.4 ns/op
With open-coded defers: 5.6 ns/op
Cost of function call alone (remove defer keyword): 4.4 ns/op

开放编码优化会在以下条件下使用:

  • 函数的 defer 少于或等于 8 个
  • 函数的 defer 关键字不能在循环中执行
  • 函数的 return 语句的数量与 defer 语句数量的乘积小于或等于 15

1. 如何启用开放编码模式

Go 语言在编译期间就确定了是否启用开放编码,

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/src/cmd/compile/internal/gc/ssa.go 
const maxOpenDefers = 8
// go/src/cmd/compile/internal/gc/walk.go
func walkstmt(n *Node) *Node {
...
switch n.Op {
...
case ODEFER:
Curfn.Func.SetHasDefer(true)
Curfn.Func.numDefers++
// 如果大于 8 个,则禁用开放编码模式
if Curfn.Func.numDefers > maxOpenDefers {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
// 处于循环中,则禁用开放编码模式
if n.Esc != EscNever {
// If n.Esc is not EscNever, then this defer occurs in a loop,
// so open-coded defers cannot be used in this function.
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
fallthrough
...
}
...
}

// go/src/cmd/compile/internal/gc/ssa.go
func buildssa(fn *Node, worker int) *ssa.Func {
...
s.hasOpenDefers = Debug['N'] == 0 && s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
...
if s.hasOpenDefers && s.curfn.Func.numReturns * s.curfn.Func.numDefers > 15 {
s.hasOpenDefers = false
}
...

}

如上代码所示,Go 在编译器生成中间代码之前,会在 walkstmt 函数中,修改已经生成的抽象语法树,设置函数体上的 OpenCodedDeferDisallowed 属性。如果函数中 defer 关键字多于 8 个或者 defer 关键字处于 for 循环中,那么就会禁用开放编码模式,而使用栈、栈上处理 defer

在 SSA 中间代码生成阶段,也可以看到 返回语句的数量与 defer 的数量的乘积如果大于 15 ,则禁用开放编码模式

如果满足上面条件,就确认使用开放编码,就会在编译期间初始化延迟比特和延迟记录

2. 延迟比特和延迟记录

如果决定使用开放编码模式,则在 SSA 中间代码生成阶段,会在栈上初始化大小为 8 比特的 deferBits 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// go/src/cmd/compile/internal/gc/ssa.go 
func buildssa(fn *Node, worker int) *ssa.Func {
...
if s.hasOpenDefers {
// Create the deferBits variable and stack slot. deferBits is a
// bitmask showing which of the open-coded defers in this function
// have been activated.
deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8])
s.deferBitsTemp = deferBitsTemp
// For this value, AuxInt is initialized to zero by default
startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])
s.vars[&deferBitsVar] = startDeferBits
s.deferBitsAddr = s.addr(deferBitsTemp)
s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)
// Make sure that the deferBits stack slot is kept alive (for use
// by panics) and stores to deferBits are not eliminated, even if
// all checking code on deferBits in the function exit can be
// eliminated, because the defer statements were all
// unconditional.
s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)
}
...
}

延迟比特中的每一个比特位都表示该位对应的 defer 关键字是否需要被执行,如果某一位被设置为 1,那么该比特位对应的函数会在函数返回前执行

因为不是函数中所有的 defer 语句都会在函数返回前执行,比如写在 if 条件判断中的 defer 语句,如下

1
2
3
4
var i = 1
if i == 0 {
defer fmt.Println("hello_world")
}

延迟比特的作用就是标记哪些 defer 关键字在函数中被执行,这样在函数返回时就可以根据对应的 deferBits 的内容确定执行的函数。而 deferBits 的大小仅为 8 字节,所以该优化的启用条件为函数中的 defer 关键字少于 8 个

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
// go/src/cmd/compile/internal/gc/ssa.go
// 传入 defer 关键字的函数和参数都会存储在 openDeferInfo 结构体
type openDeferInfo struct {
// The ODEFER node representing the function call of the defer
n *Node
// If defer call is closure call, the address of the argtmp where the
// closure is stored.
// 存储着调用的函数
closure *ssa.Value
// The node representing the argtmp where the closure is stored - used for
// function, method, or interface call, to store a closure that panic
// processing can use for this defer.
closureNode *Node
// If defer call is interface call, the address of the argtmp where the
// receiver is stored
// 存储方法的接收者
rcvr *ssa.Value
// The node representing the argtmp where the receiver is stored
rcvrNode *Node
// The addresses of the argtmps where the evaluated arguments of the defer
// function call are stored.
// 存储着函数的参数
argVals []*ssa.Value
// The nodes representing the argtmps where the args of the defer are stored
argNodes []*Node
}

如上,传入 defer 关键字的函数和参数都会存储在 openDeferInfo 结构体中。并且很多 defer 语句可以在编译期间判断是否被执行,如果函数中的 defer 语句都会在编译期间确定,中间代码生成阶段就会直接调用 openDeferExit,在函数返回前生成判断 deferBits 的代码。用一段伪代码来说明 openDefer 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
deferBits := 0 // 初始化 deferBits

_f1, _a1 := f1, a1 // 保存函数以及参数
deferBits |= 1 << 0. // 将 deferBits 最后一位设置为 1

if condition {
_f2, _a2 := f2, a2 // 保存函数以及参数
deferBits |= 1 << 1 // 将 deferBits 倒数第二位设置为 1
}
exit:

if deferBits & 1 << 1 != 0 {
deferBits &^ 1 << 1
_f2(a2)
}

if deferBits & 1 << 0 != 0 {
deferBits &^ 1 << 0
_f1(a1)
}

不过程序运行时才能判断的条件语句,仍然需要由运行的 runtime.deferreturn 决定是否执行 defer 关键字。而 runtime.deferreturn 为开放编码模式做了优化,运行时会调用 runtime.runOpenDeferFrame 执行活跃的开放编码延迟函数,如下

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
// go/src/runtime/panic.go
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
...
sp := getcallersp()
...
if d.openDefer {
done := runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
...
}

// go/src/runtime/panic.go
func runOpenDeferFrame(gp *g, d *_defer) bool {
done := true
fd := d.fd

// Skip the maxargsize
_, fd = readvarintUnsafe(fd)
deferBitsOffset, fd := readvarintUnsafe(fd)
nDefers, fd := readvarintUnsafe(fd)
deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset)))

for i := int(nDefers) - 1; i >= 0; i-- {
// read the funcdata info for this defer
var argWidth, closureOffset, nArgs uint32
argWidth, fd = readvarintUnsafe(fd)
closureOffset, fd = readvarintUnsafe(fd)
nArgs, fd = readvarintUnsafe(fd)
if deferBits&(1<<i) == 0 {
for j := uint32(0); j < nArgs; j++ {
_, fd = readvarintUnsafe(fd)
_, fd = readvarintUnsafe(fd)
_, fd = readvarintUnsafe(fd)
}
continue
}
closure := *(**funcval)(unsafe.Pointer(d.varp - uintptr(closureOffset)))
d.fn = closure
deferArgs := deferArgs(d)
// If there is an interface receiver or method receiver, it is
// described/included as the first arg.
for j := uint32(0); j < nArgs; j++ {
var argOffset, argLen, argCallOffset uint32
argOffset, fd = readvarintUnsafe(fd)
argLen, fd = readvarintUnsafe(fd)
argCallOffset, fd = readvarintUnsafe(fd)
memmove(unsafe.Pointer(uintptr(deferArgs)+uintptr(argCallOffset)),
unsafe.Pointer(d.varp-uintptr(argOffset)),
uintptr(argLen))
}
deferBits = deferBits &^ (1 << i)
*(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits
p := d._panic
reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth)
if p != nil && p.aborted {
break
}
d.fn = nil
// These args are just a copy, so can be cleared immediately
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
if d._panic != nil && d._panic.recovered {
done = deferBits == 0
break
}
}

return done
}

如上,runOpenDeferFrame 函数针对开放编码做的优化如下

  • 从 rumtime._defer 结构体中读取 deferBits、函数 defer 数量等信息
  • 在循环中依次读取函数的地址和参数信息并通过 deferBits 判断是否需要执行该函数
  • 调用 runtime.reflectcallSave 并传入要执行的 defer 函数

总结一下:开放编码主要是在中间代码生成阶段,通过延迟比特确定哪些 defer 需要运行,哪些不需要。通过 openDeferInfo 延迟记录 存储 defer 函数的地址、参数等函数信息。如果 defer 关键字的执行可以在编译期间确定,则会在函数返回前直接插入相应代码,否则会由运行时的 runtime.deferreturn 处理。