Defer
也是Go里面比较特别的一个关键字了,主要就是用来保证在程序执行过程中,defer后面的函数都会被执行到,一般用来关闭连接、清理资源等。
结构概览
defer
1 | type _defer struct { |
panic
1 | type _panic struct { |
g
因为 defer panic 都是绑定在 运行的g上的,所以这里说明一下g中与 defer panic相关的属性
1 | type g struct { |
源码分析
main
最开始,还是通过go tool
来分析一下,底层是通过什么函数来实现的吧
1 | func main() { |
go build -gcflags=all=”-N -l” main.go
go tool objdump -s “main.main” main
1 | ▶ go tool objdump -s "main\.main" main | grep CALL |
综合反编译结果可以看出,defer
关键字首先会调用 runtime.deferproc
定义一个延迟调用对象,然后再函数结束前,调用 runtime.deferreturn
来完成 defer
定义的函数的调用
panic
函数就会调用 runtime.gopanic
来实现相关的逻辑
recover
则调用 runtime.gorecover
来实现 recover 的功能
deferproc
根据 defer 关键字后面定义的函数 fn 以及 参数的size,来创建一个延迟执行的 函数,并将这个延迟函数,挂在到当前g的 _defer 的链表上
1 | func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn |
这个函数看起来比较简答,通过newproc
获取一个 _defer 的对象,并加入到当前g的 _defer 链表的头部,然后再把参数或参数的指针拷贝到 获取到的 _defer对象的 后面的内存空间
newdefer
newdefer
的作用是获取一个_defer对象, 并推入 g._defer链表的头部
1 | func newdefer(siz int32) *_defer { |
根据size获取sizeclass,对sizeclass进行分类缓存,这是内存分配时的思想
先去p上分配,然后批量从全局 sched上获取到本地缓存,这种二级缓存的思想真的是遍布在go源码的各个部分啊
deferreturn
1 | func deferreturn(arg0 uintptr) { |
freedefer
释放defer用到的函数,应该跟调度器、内存分配的思想是一样的
1 | func freedefer(d *_defer) { |
二级缓存的思想,在 深入理解Go-goroutine的实现及Scheduler分析, 深入理解go-channel和select的原理, 深入理解Go-垃圾回收机制 已经分析过了,就不再过多分析了
gopanic
1 | func gopanic(e interface{}) { |
这里解释一下 gp._panic.aborted
的作用,以下面为例
1 | func main() { |
当执行到
panic("error")
时g._defer链表: g._defer->defer2->defer1
g._panic链表:g._panic->panic1
当执行到
panic("error1")
时g._defer链表: g._defer->defer2->defer1
g._panic链表:g._panic->panic2->panic1
继续执行到 defer1 函数内部,进行recover()
此时会去恢复 panic2 引起的 panic, panic2.recovered = true,应该顺着g._panic链表继续处理下一个panic了,但是我们可以发现
panic1
已经执行过了,这也就是下面的代码的逻辑了,去掉已经执行过的panic1
2
3for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
panic的逻辑可以梳理一下:
程序在遇到panic的时候,就不再继续执行下去了,先把当前panic
挂载到 g._panic
链表上,开始遍历当前g的g._defer
链表,然后执行_defer
对象定义的函数等,如果 defer函数在调用过程中又发生了 panic,则又执行到了 gopanic
函数,最后,循环打印所有panic的信息,并退出当前g。然而,如果调用defer的过程中,遇到了recover,则继续进行调度(mcall(recovery))。
recovery
恢复一个被panic的g,重新进入并继续执行调度
1 | func recovery(gp *g) { |
gorecover
gorecovery
仅仅只是设置了 g._panic.recovered
的标志位
1 | func gorecover(argp uintptr) interface{} { |
goexit
我们还忽略了一个点,当我们手动调用 runtime.Goexit()
退出的时候,defer函数也会执行,我们分析一下这种情况
1 | func Goexit() { |
图示解析
源码这一块阅读起来难度并不是很大,如果还有什么疑惑,希望下面的一副动图能解开你的疑惑
作图作的略拙劣,见谅
步骤解析:
- L3: 生成一个defer1,放到g._defer链表上
- L11: 生成一个defer2,挂载到g._defer链表上
- L14: panic1 调用 gopanic,将当前panic放到g._panic链表上
- L14: 因为panic1,从g._defer 链表头部提取到defer2,开始执行
- L12: 执行defer2,又一个panic,挂载到g._panic链表上
- L12: 因为panic2,从g._defer链表头部提取到defer2,发现defer2已经执行过了移出链表,,且defer2是因为panic1而触发的,跳过defer2,并abort panic1
- L12: 继续提取g._defer链表的下一个,提取到defer1
- L5: defer1 执行recover,recover掉panic2,移除链表,判断下一个panic,即panic1,panic1已经被defer2 aborted掉了,移除panic1
- defer1 执行完了,移除defer1
关联文档
- 二级缓存,sizeclass: 深入理解Go-垃圾回收机制
- gogo goexit0 调度: 深入理解Go-goroutine的实现及Scheduler分析
参考文档
- 《Go语言学习笔记》–雨痕