Golang defer
作用
延迟调用
被defer的函数调用会延迟到当前函数结束的时候再进行调用,defer调用完毕后,函数的返回值才会被返回给上一级调用者。
比如这样的例子
package main
func main() {
defer println(1)
println(2)
}
其输出的结果是
2
1
后进先出调用
对defer的多次调用,是后进先出的压栈式的调用,也就是说,最后的一个defer会最先调用。
比如
package main
func main() {
for i:=0; i<3; i++ {
defer println(i)
}
}
输出结果如下:
2
1
0
defer用做recover
defer除了日常作为随后保证关闭资源的操作外,还会作为异常recover。
package main
func main() {
defer func() {
if e := recover(); e != nil {
println("recovered")
}
}()
panic("gone")
}
输出结果如下:
recovered
defer 如何起作用
我们把有defer的代码反汇编,那么可以看到如下的输出:
go tool compile -S ./main.go
...
0x0035 00053 (./main.go:5) MOVL $8, (SP)
0x003c 00060 (./main.go:5) PCDATA $0, $1
0x003c 00060 (./main.go:5) LEAQ "".wrap·1·f(SB), CX
0x0043 00067 (./main.go:5) PCDATA $0, $0
0x0043 00067 (./main.go:5) MOVQ CX, 8(SP)
0x0048 00072 (./main.go:5) MOVQ AX, 16(SP)
0x004d 00077 (./main.go:5) CALL runtime.deferproc(SB)
0x0052 00082 (./main.go:5) TESTL AX, AX
0x0054 00084 (./main.go:5) JNE 88
0x0056 00086 (./main.go:5) JMP 33
0x0058 00088 (./main.go:5) XCHGL AX, AX
0x0059 00089 (./main.go:5) CALL runtime.deferreturn(SB)
0x005e 00094 (./main.go:5) MOVQ 32(SP), BP
0x0063 00099 (./main.go:5) ADDQ $40, SP
0x0067 00103 (./main.go:5) RET
...
我们可以看到,有defer的地方被拆成了两个步骤来运行,runtime.deferproc
和runtime.deferreturn
。
defer println(1)
这句话被编译成了两个过程,先执行runtime.deferproc
生成println函数及其相关参数的描述结构体,
然后将其挂载到当前g的_defer
指针上。
deferproc
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
if getg().m.curg != getg() {
// 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(unsafe.Pointer(&siz))
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc() // 存储的是 caller 中,call deferproc 的下一条指令的地址
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:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
关键的地方是那个newdefer
,名字很好,一眼就让我们抓到了重点。
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
// 从 p 结构体的 deferpool 中获取可用的 defer struct
// 代码比较简单,省略
}
if d == nil {
// 上面没有成功获取到可用的 defer struct
// 因此需要切换到 g0 生成新的 defer struct
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
// defer func 的参数大小
d.siz = siz
// 链表链接
// 后 defer 的在前,类似一个栈结构
d.link = gp._defer
// 修改当前 g 的 defer 结构体,指向新的 defer struct
gp._defer = d
return d
}
newdefer
将一个重要的_defer
结构体挂到了goroutine的defer链上。
中间大段的注释显示了这与recover的流程关系非常大。
type _defer struct {
siz int32 // 函数的参数总大小
started bool // TODO defer 是否已开始执行?
sp uintptr // 存储调用 defer 函数的函数的 sp 寄存器值
pc uintptr // 存储 call deferproc 的下一条汇编指令的指令地址
fn *funcval // 描述函数的变长结构体,包括函数地址及参数
_panic *_panic // 正在执行 defer 的 panic 结构体
link *_defer // 链表指针
}
deferreturn
deferreturn会先判断链表中是否有defer,然后jmpdefer去做defer该干的事情, 然后,jmpdefer会的跳回deferreturn之前,如果此时defer链表中还有未处理的defer,那么就再来一把, 如果链表空了,那就return,defer的处理也就结束了。
// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
// Cannot split the stack because we reuse the caller's frame to
// call the deferred function.
// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp(unsafe.Pointer(&arg0))
if d.sp != sp {
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
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)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
我们来看看对链表进行循环遍历的jmpdefer
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // defer 的函数的地址
MOVQ argp+8(FP), BX // caller sp
LEAQ -8(BX), SP // caller sp after CALL
MOVQ -8(SP), BP // 在 framepointer enable 的时候,不影响函数栈的结构
SUBQ $5, (SP) // call 指令长度为 5,因此通过将 ret addr 减 5,能够使 deferreturn 自动被反复调用
MOVQ 0(DX), BX
JMP BX // 调用被 defer 的函数
哈,我们可以看到,在jmpdefer所调用的函数返回时,会回到调用deferreturn的函数, 并重新执行deferreturn,每次执行都会使g的defer链表表头被消耗掉, 直到进入deferreturn时d == nil并返回。至此便完成了整个defer的流程。
这里比较粗暴的是直接把栈上存储的pc寄存器的值减了5, 注释中说是因为call deferreturn这条指令长度为5,这是怎么算出来的呢:
0x0059 00089 (./main.go:5) CALL runtime.deferreturn(SB)
0x005e 00094 (./main.go:5) MOVQ 32(SP), BP
这条指令长度就是5,所以这里是用汇编非常trick的实现了一个for循环…
那么deferreturn + jmpdefer就可以使_defer链表被消费完,那为什么还要编译出多次的deferreturn调用呢? 可能是因为deferproce和deferreturn是成对出现的,这样做可能比较容易实现吧。
总结
defer是一个面向编译器的声明,他会让编译器做两件事:
- 编译器会将defer声明编译为runtime.deferproc(fn),这样运行时,会调用runtime.deferproc,在deferproc中将所有defer挂到goroutine的defer链上
- 编译器会在函数return之前,增加runtime.deferreturn调用;这样运行时,开始处理前面挂在defer链上的所有defer。