6.2 深入理解 Go defer
在上一章节 《深入理解 Go panic and recover》 中,我们发现了 defer
与其关联性极大,还是觉得非常有必要深入一下。希望通过本章节大家可以对 defer
关键字有一个深刻的理解,那么我们开始吧。你先等等,请排好队,我们这儿采取后进先出 LIFO 的出站方式…
特性
我们简单的过一下 defer
关键字的基础使用,让大家先有一个基础的认知
一、延迟调用
func main() {
defer log.Println("EDDYCJY.")
log.Println("end.")
}
输出结果:
$ go run main.go
2019/05/19 21:15:02 end.
2019/05/19 21:15:02 EDDYCJY.
二、后进先出
func main() {
for i := 0; i < 6; i++ {
defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".")
}
log.Println("end.")
}
输出结果:
$ go run main.go
2019/05/19 21:19:17 end.
2019/05/19 21:19:17 EDDYCJY5.
2019/05/19 21:19:17 EDDYCJY4.
2019/05/19 21:19:17 EDDYCJY3.
2019/05/19 21:19:17 EDDYCJY2.
2019/05/19 21:19:17 EDDYCJY1.
2019/05/19 21:19:17 EDDYCJY0.
三、运行时间点
func main() {
func() {
defer log.Println("defer.EDDYCJY.")
}()
log.Println("main.EDDYCJY.")
}
输出结果:
$ go run main.go
2019/05/22 23:30:27 defer.EDDYCJY.
2019/05/22 23:30:27 main.EDDYCJY.
四、异常处理
func main() {
defer func() {
if e := recover(); e != nil {
log.Println("EDDYCJY.")
}
}()
panic("end.")
}
输出结果:
$ go run main.go
2019/05/20 22:22:57 EDDYCJY.
源码剖析
$ go tool compile -S main.go
"".main STEXT size=163 args=0x0 locals=0x40
...
0x0059 00089 (main.go:6) MOVQ AX, 16(SP)
0x005e 00094 (main.go:6) MOVQ $1, 24(SP)
0x0067 00103 (main.go:6) MOVQ $1, 32(SP)
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB)
0x0075 00117 (main.go:6) TESTL AX, AX
0x0077 00119 (main.go:6) JNE 137
0x0079 00121 (main.go:7) XCHGL AX, AX
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
0x0084 00132 (main.go:7) ADDQ $64, SP
0x0088 00136 (main.go:7) RET
0x0089 00137 (main.go:6) XCHGL AX, AX
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB)
0x008f 00143 (main.go:6) MOVQ 56(SP), BP
0x0094 00148 (main.go:6) ADDQ $64, SP
0x0098 00152 (main.go:6) RET
...
首先我们需要找到它,找到它实际对应什么执行代码。通过汇编代码,可得知涉及如下方法:
- runtime.deferproc
- runtime.deferreturn
很显然是运行时的方法,是对的人。我们继续往下走看看都分别承担了什么行为
数据结构
在开始前我们需要先介绍一下 defer
的基础单元 _defer
结构体,如下:
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
...
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
- siz:所有传入参数的总大小
- started:该
defer
是否已经执行过 - sp:函数栈指针寄存器,一般指向当前函数栈的栈顶
- pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令
- fn:指向传入的函数地址和参数
- _panic:指向
_panic
链表 - link:指向
_defer
链表
deferproc
func deferproc(siz int32, fn *funcval) {
...
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
...
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))
}
return0()
}
- 获取调用
defer
函数的函数栈指针、传入函数的参数具体地址以及PC (程序计数器),也就是下一个要执行的指令。这些相当于是预备参数,便于后续的流转控制 - 创建一个新的
defer
最小单元_defer
,填入先前准备的参数 - 调用
memmove
将传入的参数存储到新_defer
(当前使用)中去,便于后续的使用 - 最后调用
return0
进行返回,这个函数非常重要。能够避免在deferproc
中又因为返回return
,而诱发deferreturn
方法的调用。其根本原因是一个停止panic
的延迟方法会使deferproc
返回 1,但在机制中如果deferproc
返回不等于 0,将会总是检查返回值并跳转到函数的末尾。而return0
返回的就是 0,因此可以防止重复调用
小结
在这个函数中会为新的 _defer
设置一些基础属性,并将调用函数的参数集传入。最后通过特殊的返回方法结束函数调用。另外这一块与先前 《深入理解 Go panic and recover》 的处理逻辑有一定关联性,其实就是 gp.sched.ret
返回 0 还是 1 会分流至不同处理方式
newdefer
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 {
...
lock(&sched.deferlock)
d := sched.deferpool[sc]
unlock(&sched.deferlock)
}
...
}
if d == nil {
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
...
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
- 从池中获取可以使用的
_defer
,则复用作为新的基础单元 - 若在池中没有获取到可用的,则调用
mallocgc
重新申请一个新的 - 设置
defer
的基础属性,最后修改当前Goroutine
的_defer
指向
通过这个方法我们可以注意到两点,如下:
defer
与Goroutine(g)
有直接关系,所以讨论defer
时基本离不开g
的关联- 新的
defer
总是会在现有的链表中的最前面,也就是defer
的特性后进先出
小结
这个函数主要承担了获取新的 _defer
的作用,它有可能是从 deferpool
中获取的,也有可能是重新申请的
deferreturn
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
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)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
如果在一个方法中调用过 defer
关键字,那么编译器将会在结尾处插入 deferreturn
方法的调用。而该方法中主要做了如下事项:
- 清空当前节点
_defer
被调用的函数调用信息 - 释放当前节点的
_defer
的存储信息并放回池中(便于复用) - 跳转到调用
defer
关键字的调用函数处
在这段代码中,跳转方法 jmpdefer
格外重要。因为它显式的控制了流转,代码如下:
// asm_amd64.s
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn
MOVQ argp+8(FP), BX // caller sp
LEAQ -8(BX), SP // caller sp after CALL
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
SUBQ $5, (SP) // return to CALL again
MOVQ 0(DX), BX
JMP BX // but first run the deferred function
通过源码的分析,我们发现它做了两个很 “奇怪” 又很重要的事,如下:
- MOVQ -8(SP), BP:
-8(BX)
这个位置保存的是deferreturn
执行完毕后的地址 - SUBQ $5, (SP):
SP
的地址减 5 ,其减掉的长度就恰好是runtime.deferreturn
的长度
你可能会问,为什么是 5?好吧。翻了半天最后看了一下汇编代码…嗯,相减的确是 5 没毛病,如下:
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
我们整理一下思绪,照上述逻辑的话,那 deferreturn
就是一个 “递归” 了哦。每次都会重新回到 deferreturn
函数,那它在什么时候才会结束呢,如下:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
...
}
也就是会不断地进入 deferreturn
函数,判断链表中是否还存着 _defer
。若已经不存在了,则返回,结束掉它。简单来讲,就是处理完全部 defer
才允许你真的离开它。果真如此吗?我们再看看上面的汇编代码,如下:
。..
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB)
0x0075 00117 (main.go:6) TESTL AX, AX
0x0077 00119 (main.go:6) JNE 137
0x0079 00121 (main.go:7) XCHGL AX, AX
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
0x0084 00132 (main.go:7) ADDQ $64, SP
0x0088 00136 (main.go:7) RET
0x0089 00137 (main.go:6) XCHGL AX, AX
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB)
...
的确如上述流程所分析一致,验证完毕
小结
这个函数主要承担了清空已使用的 defer
和跳转到调用 defer
关键字的函数处,非常重要
总结
我们有提到 defer
关键字涉及两个核心的函数,分别是 deferproc
和 deferreturn
函数。而 deferreturn
函数比较特殊,是当应用函数调用 defer
关键字时,编译器会在其结尾处插入 deferreturn
的调用,它们俩一般都是成对出现的
但是当一个 Goroutine
上存在着多次 defer
行为(也就是多个 _defer
)时,编译器会进行利用一些小技巧, 重新回到 deferreturn
函数去消耗 _defer
链表,直到一个不剩才允许真正的结束
而新增的基础单元 _defer
,有可能是被复用的,也有可能是全新申请的。它最后都会被追加到 _defer
链表的表头,从而设定了后进先出的调用特性