5.3 defer
5.3.1 现象
使用defer时会出现两个常见的问题:
defer的调用时机和调用顺序defer会使用值传递的方式传参时进行预计算,导致预期结果不符
作用域
defer语句会在函数返回之前执行,并按照后进先出的顺序执行。
func main() {
for i := 0; i < 5; i++ {
defer println(i)
}
}
$ go run main.go
4
3
2
1
0
上述代码可以看出,defer的执行顺序是按照后进先出的顺序的。
func main() {
{
defer println("defer runs")
println("block ends")
}
println("func ends")
}
$ go run main.go
block ends
main ends
defer runs
可以看出,defer是在函数返回之前被调用。
预计算参数
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
$ go run main.go
0s
调用 defer 关键字会立刻拷贝函数中引用的外部参数, time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。
若使用defer调用匿名函数:
func main() {
startedAt := time.Now()
defer func() { fmt.Println(time.Since(startedAt)) }()
time.Sleep(time.Second)
}
$ go run main.go
1s
此时调用defer时,拷贝的是函数的指针,对函数内部的参数没有影响,可得到预期的结果。
5.3.2 数据结构
defer的数据结构用runtime._defer表示,主要字段如下:
type _defer struct {
siz int32
started bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
...
}
siz:参数和结果的内存大小sp:栈指针pc:程序计数器fn:defer关键字中传入的函数_panic:触发延迟调用的结构体openDefer:当前defer是否经过开放编码优化link:下一个_defer节点
runtime._defer之间使用链表的形式串联起来:

5.3.3 执行机制
中间代码生成阶段会使用cmd/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.callResult(n.Left, d)
}
}
}
5.3.4 堆上分配
当使用堆分配时,编译器会调用 cmd/compile/internal/gc.state.callResult 和 cmd/compile/internal/gc.state.call进行处理(实际上defer是被按照函数调用进行处理的)。
cmd/compile/internal/gc.state.call负责为所有函数和方法调用生成中间代码,流程如下:
- 获取需要执行的函数名、闭包指针、代码指针和函数调用的接收方
- 获取栈地址并将函数或者方法的参数写入栈中
- 使用
cmd/compile/internal/gc.state.newValue1A以及相关函数生成函数调用的中间代码 - 若当前调用的函数是
defer,那么会单独生成相关的结束代码块 - 获取函数的返回值地址并结束当前调用
func (s *state) call(n *Node, k callKind, returnResultAddr bool) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上初始化 defer 结构体
...
} else {
...
switch {
case k == callDefer:
aux := ssa.StaticAuxCall(deferproc, ACArgs, ACResults)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
...
}
call.AuxInt = stksize
}
s.vars[&memVar] = call
...
}
编译器将 defer 关键字都转换成 runtime.deferproc 函数,并通过以下三个步骤为所有调用 defer 的函数末尾插入 runtime.deferreturn 的函数调用:
cmd/compile/internal/gc.walkstmt在遇到ODEFER节点时会执行Curfn.Func.SetHasDefer(true)设置当前函数的hasdefer属性cmd/compile/internal/gc.buildssa会执行s.hasdefer = fn.Func.HasDefer()更新state的hasdefercmd/compile/internal/gc.state.exit会根据state的hasdefer在函数返回之前插入runtime.deferreturn的函数调用
func (s *state) exit() *ssa.Block {
if s.hasdefer {
...
s.rtcall(Deferreturn, true, nil)
}
...
}
上述两个运行时函数是 defer 关键字运行时机制的入口,分别承担了不同的工作:
runtime.deferproc负责创建新的延迟调用;runtime.deferreturn负责在函数调用结束时执行所有的延迟调用;
创建延迟调用
runtime.deferproc 为 defer 创建新的 runtime._defer 结构体、设置函数指针 fn、程序计数器 pc 和栈指针 sp 并将相关的参数拷贝到相邻的内存空间中:
func deferproc(siz int32, fn *funcval) {
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,可以避免递归 runtime.deferreturn 的递归调用。
runtime.deferproc 中 runtime.newdefer 的作用是获得 runtime._defer 结构体,包含三种路径:
- 从调度器的延迟调用缓存池
sched.deferpool中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中 - 从 Goroutine 的延迟调用缓存池
pp.deferpool中取出结构体 - 通过
runtime.mallocgc在堆上创建一个新的结构体
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()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
}
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 {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
三种方式最终都会将获取到 runtime._defer 结构体追加到所在 Goroutine _defer 链表的最前面。

可以看出defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的。
执行延迟调用
runtime.deferreturn 会从 Goroutine 的 _defer 链表中取出最前面的 runtime._defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
...
switch d.siz {
case 0:
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
runtime.jmpdefer主要工作是跳转到 defer 所在的代码段并在执行结束之后跳转回 runtime.deferreturn:
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8
MOVL fv+0(FP), DX // fn
MOVL argp+4(FP), BX // caller sp
LEAL -4(BX), SP // caller sp after CALL
#ifdef GOBUILDMODE_shared
SUBL $16, (SP) // return to CALL again
#else
SUBL $5, (SP) // return to CALL again
#endif
MOVL 0(DX), BX
JMP BX // but first run the deferred function
runtime.deferreturn 会多次判断当前 Goroutine 的 _defer 链表中是否有未执行的结构体,该函数只有在所有延迟函数都执行后才会返回。
5.3.5 栈上分配
当使用栈上分配时,可以节约内存分配带来的额外开销,与堆上分配的 runtime._defer 相比,该方法可以将 defer 关键字的额外开销降低 ~30%。。
在go v1.13 中,对 defer 关键字进行了优化,当该关键字在函数体中最多执行一次时,编译期间的 cmd/compile/internal/gc.state.call 会将结构体分配到栈上并调用 runtime.deferprocStack:
func (s *state) call(n *Node, k callKind) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上创建 _defer 结构体
t := deferstruct(stksize)
...
ACArgs = append(ACArgs, ssa.Param{Type: types.Types[TUINTPTR], Offset: int32(Ctxt.FixedFrameSize())})
aux := ssa.StaticAuxCall(deferprocStack, ACArgs, ACResults) // 调用 deferprocStack
arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize())
s.store(types.Types[TUINTPTR], arg0, addr)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
call.AuxInt = stksize
} else {
...
}
s.vars[&memVar] = call
...
}
在编译期间已经创建了 runtime._defer 结构体,所以在运行期间 runtime.deferprocStack 只需要设置一些未在编译期间初始化的字段,就可以将栈上的 runtime._defer 追加到函数的链表上:
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false // 栈上分配的 _defer
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))
return0()
}
5.3.5 开放编码
在go v1.14之后,通过开放编码(Open Coded)实现defer关键字,该设计使用代码内联优化 defer 关键的额外开销并引入函数数据 funcdata 管理 panic 的调用,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右:
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 个
启用优化
程序在编译期间会确定是否启用开放编码,编译器生成中间代码之前,会使用 cmd/compile/internal/gc.walkstmt 修改已经生成的抽象语法树,设置函数体上的 OpenCodedDeferDisallowed 属性:
const maxOpenDefers = 8
func walkstmt(n *Node) *Node {
switch n.Op {
case ODEFER:
Curfn.Func.SetHasDefer(true)
Curfn.Func.numDefers++
if Curfn.Func.numDefers > maxOpenDefers {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
if n.Esc != EscNever {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
fallthrough
...
}
}
在 SSA 中间代码生成阶段的 cmd/compile/internal/gc.buildssa 中,能够看到启用开放编码优化的其他条件,也就是返回语句的数量与 defer 数量的乘积需要小于 15:
func buildssa(fn *Node, worker int) *ssa.Func {
...
s.hasOpenDefers = s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
...
if s.hasOpenDefers &&
s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {
s.hasOpenDefers = false
}
...
}
中间代码生成的这两个步骤会决定当前函数是否应该使用开放编码优化 defer 关键字,一旦确定使用开放编码,就会在编译期间初始化延迟比特和延迟记录。
延迟记录
延迟比特和延迟记录是使用开放编码实现 defer 的两个最重要结构,一旦决定使用开放编码,cmd/compile/internal/gc.buildssa 会在编译期间在栈上初始化大小为 8 个比特的 deferBits 变量:
func buildssa(fn *Node, worker int) *ssa.Func {
...
if s.hasOpenDefers {
deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8]) // 初始化延迟比特
s.deferBitsTemp = deferBitsTemp
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)
s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)
}
}
延迟比特中的每一个比特位都表示该位对应的 defer 关键字是否需要被执行,如下图所示,其中 8 个比特的倒数第二个比特在函数返回前被设置成了 1,那么该比特位对应的函数会在函数返回前执行:

因为不是函数中所有的 defer 语句都会在函数返回前执行,如下所示的代码只会在 if 语句的条件为真时,其中的 defer 语句才会在结尾被执行
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)
}
延迟比特的作用就是标记哪些 defer 关键字在函数中被执行,这样在函数返回时可以根据对应 deferBits 的内容确定执行的函数,而正是因为 deferBits 的大小仅为 8 比特,所以该优化的启用条件为函数中的 defer 关键字少于 8 个。
上述伪代码展示了开放编码的实现原理,但是仍然缺少了一些细节,例如:传入 defer 关键字的函数和参数都会存储在如下所示的 cmd/compile/internal/gc.openDeferInfo 结构体中:
type openDeferInfo struct {
n *Node
closure *ssa.Value
closureNode *Node
rcvr *ssa.Value
rcvrNode *Node
argVals []*ssa.Value
argNodes []*Node
}
当编译器在调用 cmd/compile/internal/gc.buildssa 构建中间代码时会通过 cmd/compile/internal/gc.state.openDeferRecord 方法在栈上构建结构体,该结构体的 closure 中存储着调用的函数,rcvr 中存储着方法的接收者,而最后的 argVals 中存储了函数的参数。
很多 defer 语句都可以在编译期间判断是否被执行,若函数中的 defer 语句都会在编译期间确定,中间代码生成阶段就会直接调用 cmd/compile/internal/gc.state.openDeferExit 在函数返回前生成判断 deferBits 的代码,也就是上述伪代码中的后半部分。
不过当程序遇到运行时才能判断的条件语句时,我们仍然需要由运行时的 runtime.deferreturn 决定是否执行 defer 关键字:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
sp := getcallersp()
if d.openDefer {
runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
...
}
该函数为开放编码做了特殊的优化,运行时会调用 runtime.runOpenDeferFrame 执行活跃的开放编码延迟函数,该函数会执行以下的工作:
- 从
runtime._defer结构体中读取deferBits、函数defer数量等信息; - 在循环中依次读取函数的地址和参数信息并通过
deferBits判断该函数是否需要被执行; - 调用
runtime.reflectcallSave调用需要执行的defer函数;
func runOpenDeferFrame(gp *g, d *_defer) bool {
fd := d.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-- {
var argWidth, closureOffset, nArgs uint32 // 读取函数的地址和参数信息
argWidth, fd = readvarintUnsafe(fd)
closureOffset, fd = readvarintUnsafe(fd)
nArgs, fd = readvarintUnsafe(fd)
if deferBits&(1<<i) == 0 {
...
continue
}
closure := *(**funcval)(unsafe.Pointer(d.varp - uintptr(closureOffset)))
d.fn = closure
...
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
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
...
}
return done
}
5.3.7 小结
defer 关键字的实现有三种机制:
- 堆上分配 · 1.1 ~ 1.12
- 编译期将
defer关键字转换成runtime.deferproc并在调用defer关键字的函数返回之前插入runtime.deferreturn - 运行时调用
runtime.deferproc会将一个新的runtime._defer结构体追加到当前 Goroutine 的链表头 - 运行时调用
runtime.deferreturn会从 Goroutine 的链表中取出runtime._defer结构并依次执行
- 编译期将
- 栈上分配 · 1.13
- 当该关键字在函数体中最多执行一次时,编译期间的
cmd/compile/internal/gc.state.call会将结构体分配到栈上并调用runtime.deferprocStack
- 当该关键字在函数体中最多执行一次时,编译期间的
- 开放编码 · 1.14
- 编译期间判断
defer关键字、return语句的个数确定是否开启开放编码优化 - 通过
deferBits和cmd/compile/internal/gc.openDeferInfo存储defer关键字的相关信息 - 若
defer关键字的执行可以在编译期间确定,会在函数返回前直接插入相应的代码,否则会由运行时的runtime.deferreturn处理
- 编译期间判断
defer执行顺序和预先计算的原理:
- 后调用的
defer函数会先执行:- 后调用的
defer函数会被追加到 Goroutine_defer链表的最前面 - 运行
runtime._defer时是从前到后依次执行
- 后调用的
- 函数的参数会被预先计算;
- 调用
runtime.deferproc函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算
- 调用
