上篇文章中我们讲到栈的时候说到先入后出这种特性,在 Go 中第一时间想到的就是 defer 接下来我们就深入理解一下 defer
用法
下面先回顾一下基本的用法以及较为常见的坑,文末会给出输出结果,可以先想想会输出什么
基本用法 1: 延迟处理,资源清理
// 基本用法:延迟调用,清理资源
func f0() {
defer fmt.Println("clean")
fmt.Println("hello")
}
基本用法 2: 后进先出
// 基本用法1: 后进先出
func f1() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
}
基本用法 3: 异常恢复
// 基本用法2:异常恢复
func f2() {
defer func() {
if err: = recover();
err != nil {
fmt.Printf("paniced: %+v \n", err)
}
}()
panic("test")
}
容易掉坑 1: 闭包变量
// 容易掉坑之,函数变量修改
func f3()(res int) {
defer func() {
res++
}()
return 0
}
容易掉坑 2: 参数传递
// 容易掉坑之,参数复制
func f4()(res int) {
defer func(res int) {
res++
}(res)
return 0
}
源码剖析
想要看源码,我们需要先找到源码的位置,我们可以直接执行 go tool compile -N -l -S main.go 获取汇编代码
// ....
0x00d8 00216 (main_2.go:6) PCDATA $1, $0
0x00d8 00216 (main_2.go:6) CALL runtime.deferprocStack(SB)
0x00dd 00221 (main_2.go:6) NOP
0x00e0 00224 (main_2.go:6) TESTL AX, AX
0x00e2 00226 (main_2.go:6) JNE 252
0x00e4 00228 (main_2.go:6) JMP 230
0x00e6 00230 (main_2.go:7) XCHGL AX, AX
0x00e7 00231 (main_2.go:7) CALL runtime.deferreturn(SB)
0x00ec 00236 (main_2.go:7) MOVQ 216(SP), BP
0x00f4 00244 (main_2.go:7) ADDQ $224, SP
0x00fb 00251 (main_2.go:7) RET
0x00fc 00252 (main_2.go:6) XCHGL AX, AX
0x00fd 00253 (main_2.go:6) NOP
0x0100 00256 (main_2.go:6) CALL runtime.deferreturn(SB)
我们可以看到主要是调用了 runtime.deferprocStack , runtime.deferreturn 这两个运行时的方法
defer 定义
type _defer struct {
siz int32 // 所有传入参数和返回值的总大小
started bool // defer 是否执行了
heap bool // 是否在堆上,这是 go1.13 新加的,划重点
sp uintptr // 函数栈指针寄存器,一般指向当前函数栈的栈顶
pc uintptr // 程序计数器,指向下一条需要执行的指令
fn * funcval // 指向传入的函数地址和参数
_panic * _panic // 指向 panic 链表
link * _defer // 指向 defer 链表
//...
}
deferprocStack
func deferprocStack(d * _defer) {
gp: = getg() // 获取 g,判断是否在用户栈上
if gp.m.curg != gp {
// go code on the system stack can't defer
throw ("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer // 这两个是将当前 defer 插入到链表头部,也就是defer为什么时候先入后出的原因
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
* ( * 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()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
注意这几行
说明这个 defer 不在堆上
d.heap = false
这两个是将当前 defer 插入到链表头部,也就是 defer 为什么时候先入后出的原因
// d.link = gp._defer
// gp._defer = d
deferreturn
func deferreturn(arg0 uintptr) {
gp: = getg()
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)
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer( & arg0)))
}
如果函数中存在 defer 编译器就会自动在函数的最后插入一个 deferreturn:
1.清空 defer 的调用信息
2.freedefer 将 defer 对象放入到 defer 池中,后面可以复用
3.如果存在延迟函数就会调用 runtime·jmpdefer 方法跳转到对应的方法上去
4.runtime·jmpdefer 方法会递归调用 deferreturn 一直执行到结束为止
deferproc
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()
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))
}
// 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.
}
除了 deferprocStack 还有 deferproc 这个方法,那这个方法和之前的方法有什么区别呢?
主要的区别就是这个方法将 defer 分配在了堆上,看下方的 newdefer
func newdefer(siz int32) *_defer {
// ...
d.heap = true
return d
}
其他和 deferprocStack 类似这里就不赘述了
什么时候 defer 会在堆上什么时候会在栈上?
那问题来了如何判断 defer 在堆上还是在栈上呢?
https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/escape.go#L743
topLevelDefer := where != nil && where.Op == ODEFER && e.loopDepth == 1
if topLevelDefer {
// force stack allocation of defer record, unless
// open-coded defers are used (see ssa.go)
where.Esc = EscNever
}
https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/cmd/compile/internal/gc/ssa.go#L1116
d: = callDefer
if n.Esc == EscNever {
d = callDeferStack
}
s.call(n.Left, d)
可以看到主要是在逃逸分析的时候,发现 e.loopDepth == 1 并且不是 open-coded defer 就会分配到栈上。
这也是为什么 go 1.13 之后 defer 性能提升的原因,所以切记不要在循环中使用 defer 不然优化也享受不到
我们来验证一下
func f6() {
for i: = 0;
i < 10;
i++{
defer func() {
fmt.Printf("f6: %d\n", i)
}()
}
}
看一下汇编结果
0x0073 00115 (main.go:67) CALL runtime.deferproc(SB)
0x0078 00120 (main.go:67) TESTL AX, AX
0x007a 00122 (main.go:67) JNE 151
0x007c 00124 (main.go:67) JMP 126
0x007e 00126 (main.go:66) PCDATA $1, $-1
0x007e 00126 (main.go:66) NOP
0x0080 00128 (main.go:66) JMP 130
0x0082 00130 (main.go:66) MOVQ "".&i+32(SP), AX
0x0087 00135 (main.go:66) MOVQ (AX), AX
0x008a 00138 (main.go:66) MOVQ "".&i+32(SP), CX
0x008f 00143 (main.go:66) INCQ AX
0x0092 00146 (main.go:66) MOVQ AX, (CX)
0x0095 00149 (main.go:66) JMP 68
0x0097 00151 (main.go:67) PCDATA $1, $0
0x0097 00151 (main.go:67) XCHGL AX, AX
0x0098 00152 (main.go:67) CALL runtime.deferreturn(SB)
可以发现在循环嵌套的场景下,的确调用的是 runtime.deferproc 方法,被分配到栈上了