golang panic和recover 实现原理
这篇文章是系列文章中的第二篇,系列文章主要包括:
- golang defer的原理
- golang panic和recover()函数的原理(包括golang对于错误处理方式)
- defer性能损耗的讨论以及最重要的应用场景
- defer在golang 1.13 上的性能
panic 能中断一个程序的执行,同时也能在一定情况下进行恢复(recover)。我们就来看一看 panic 和 recover 这对关键字的实现机制。根据我们对 Go 的实践,可以预见的是,他们的实现跟runtime调度器和 defer 关键字也紧密相关。
1.为什么go 进程会终止
func main() { panic("sim lou.") }
输出结果是:
panic: sim lou. goroutine 1 [running]: main.main() /Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test1.go:4 +0x39 Process finished with exit code 2
这里思考一下,为什么执行 panic 后会导致应用程序运行中止?或者说执行panic为什么导致进程终止了?
2. 为什么不会中止运行
func main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("sim lou.") }
输出结果是:
2019/10/26 22:19:33 recover: sim lou. Process finished with exit code 0
思考一下为什么加上 defer + recover 组合就可以保护应用程序不会退出。
3.不设置 defer 行不
上面问题二是 defer + recover 组合,那我去掉 defer 是不是也可以呢?如下:
func main() { if err := recover(); err != nil { log.Printf("recover: %v", err) } panic("sim lou.") }
运行结果:
panic: sim lou. goroutine 1 [running]: main.main() /Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test3.go:9 +0xa1 Process finished with exit code 2
不行!!!我们常说 defer + recover 组合 “万能” 捕获。但是为什么呢。去掉 defer 后为什么就无法捕获了?
思考一下,为什么需要设置 defer 后 recover 才能起作用?
同时你还需要仔细想想,我们设置 defer + recover 组合后就能无忧无虑了吗,各种 “乱” 写了吗?
4. 为什么起个 goroutine 就不行
func main() { go func() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() }() panic("qwertyuiop.") }
输出结果:
panic: qwertyuiop. goroutine 1 [running]: main.main() /Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test4.go:13 +0x51
请思考一下,为什么新起了一个 Goroutine 就无法捕获到异常了?到底发生了什么事…
但是我们改一下:
func main() { go func() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("aim lou 2.") }() time.Sleep(1* time.Second) }
输出是:
2019/10/26 22:27:50 recover: aim lou 2.
为什么我们把panic放到自定义的协程里面recover又可以work了呢?
基于前面的四个问题,我们阅读源码,从源码找到root cause.
type _panic struct { argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink arg interface{} // argument to panic link *_panic // link to earlier panic recovered bool // whether this panic is over aborted bool // the panic was aborted }
在 panic 中是使用 _panic 作为其基础单元的,每执行一次 panic 语句,都会创建一个 _panic 对象。它包含了一些基础的字段用于存储当前的 panic 调用情况,涉及的字段如下
- argp:指向 defer 延迟调用的参数的指针
- arg:panic 的原因,也就是调用 panic 时传入的参数
- link:指向上一个调用的 _panic,这里说明panic也是一个链表
- recovered:panic 是否已经被处理过,也就是是否被 recover
- aborted:panic 是否被中止
通过查看 link 字段,可得知其是一个链表的数据结构,如下图:
+-----------+ +-----------+ +-----------+ | _panic | +----> _panic | +-----> _panic | +-----------+ | +-----------+ | +-----------+ | ...... | | | ...... | | | ...... | +-----------+ | +-----------+ | +-----------+ | link |-----+ | link +----+ | link | +-----------+ +-----------+ +-----------+ | ...... | | ...... | | ...... | +-----------+ +-----------+ +-----------+
我们先看看panic生成的汇编代码:
func main() { panic("sim lou.") }
汇编代码:
"".main STEXT size=65 args=0x0 locals=0x18 0x0000 00000 (panic_test1.go:3) TEXT "".main(SB), ABIInternal, $24-0 ...... 0x002f 00047 (panic_test1.go:4) PCDATA $0, $0 0x002f 00047 (panic_test1.go:4) MOVQ AX, 8(SP) 0x0034 00052 (panic_test1.go:4) CALL runtime.gopanic(SB) .......
可以看到 panic 翻译成汇编代码主要是调用了 runtime.gopanic,我们一起来看看这个方法做了什么事,如下(省略部分)
func gopanic(e interface{}) { gp := getg() ...... var p _panic p.arg = e // 头插法 p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) for { d := gp._defer if d == nil { break } // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic), // take defer off list. The earlier panic or Goexit will not continue running. if d.started { if d._panic != nil { d._panic.aborted = true } d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) continue } // Mark defer as started, but keep on list, so that traceback // can find and update the defer's argument frame if stack growth // or a garbage collection happens before reflectcall starts executing d.fn. d.started = true // Record the panic that is running the defer. // If there is a new panic during the deferred call, that panic // will find d in the list and will mark d._panic (this panic) aborted. d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) p.argp = unsafe.Pointer(getargp(0)) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) p.argp = nil // reflectcall did not panic. Remove d. if gp._defer != d { throw("bad defer entry in panic") } d._panic = nil d.fn = nil gp._defer = d.link // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic //GC() pc := d.pc sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy freedefer(d) if p.recovered { atomic.Xadd(&runningPanicDefers, -1) gp._panic = p.link // Aborted panics are marked but remain on the g.panic list. // Remove them from the list. for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { // must be done with signal gp.sig = 0 } // Pass information about recovering frame to recovery. gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") // mcall should not return } } preprintpanics(gp._panic) fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached }
- 获取指向当前 Goroutine 的指针
- 初始化一个 panic 的基本单位 _panic,并将这个panic头插入当前goroutine的panic链表中。
- 获取当前 Goroutine 上挂载的 _defer(数据结构也是链表)
- 若当前存在 defer 调用,则调用 reflectcall 方法去执行先前 defer 中延迟执行的代码。reflectcall方法若在执行过程中需要运行 recover 将会调用 gorecover 方法。
- 结束前,使用 preprintpanics 方法打印出所涉及的 panic 消息
- 最后调用 fatalpanic 中止应用程序,实际是执行 exit(2) 进行最终退出行为的。
通过对上述代码的执行分析,可得知 panic 方法实际上就是处理当前 Goroutine(g) 上所挂载的 ._panic 链表(所以无法对其他 Goroutine 的异常事件响应),然后对其所属的 defer 链表和 recover 进行检测并处理,最后调用退出命令中止应用程序。
func main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("sim lou.") }
输出结果:
2019/10/27 12:39:30 recover: sim lou. Process finished with exit code 0
我们看汇编代码,panic是怎么被recover的:
"".main STEXT size=118 args=0x0 locals=0x50 ...... 0x003a 00058 (panic_test2.go:6) CALL runtime.deferprocStack(SB) ...... 0x005a 00090 (panic_test2.go:12) CALL runtime.gopanic(SB) ...... 0x0060 00096 (panic_test2.go:6) CALL runtime.deferreturn(SB) ...... "".main.func1 STEXT size=151 args=0x0 locals=0x40 0x0000 00000 (panic_test2.go:6) TEXT "".main.func1(SB), ABIInternal, $64-0 ...... 0x0026 00038 (panic_test2.go:7) CALL runtime.gorecover(SB) ...... 0x0092 00146 (panic_test2.go:6) JMP 0
通过分析底层调用,可得知主要是如下几个方法:
- runtime.deferprocStack
- runtime.gopanic
- runtime.deferreturn
- runtime.gorecover
前面我们说了简单的流程,gopanic 方法会遍历调用当前 Goroutine 下的 defer 链表,若 reflectcall 执行中遇到 recover 就会调用 gorecover 进行处理,该方法代码如下:
func gorecover(argp uintptr) interface{} { // Must be in a function running as part of a deferred call during the panic. // Must be called from the topmost function of the call // (the function used in the defer statement). // p.argp is the argument pointer of that topmost deferred function call. // Compare against argp reported by caller. // If they match, the caller is the one who can recover. gp := getg() p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
这代码,看上去挺简单的,核心就是修改 recovered 字段。该字段是用于标识当前 panic 是否已经被 recover 处理。但是这和我们想象的并不一样啊,程序是怎么从 panic 流转回去的呢?是不是在核心方法里处理了呢?我们再看看 gopanic 的代码,如下:
func gopanic(e interface{}) { ... for { // defer... ... pc := d.pc sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy freedefer(d) // recover... if p.recovered { atomic.Xadd(&runningPanicDefers, -1) gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") } } ... }
我们回到 gopanic 方法中再仔细看看,发现实际上是包含对 recover 流转的处理代码的。恢复流程如下:
- 判断当前 _panic 中的 recover 是否已标注为处理
- 从 _panic 链表中删除已标注中止的 panic 事件,也就是删除已经被恢复的 panic 事件
- 将相关需要恢复的栈帧信息传递给 recovery 方法的 gp 参数(每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量)
- 执行 recovery 进行恢复动作
- 从流程来看,最核心的是 recovery 方法。它承担了异常流转控制的职责。代码如下:
func recovery(gp *g) { // Info about defer passed in G struct. sp := gp.sigcode0 pc := gp.sigcode1 // d's arguments need to be in the stack. if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) { print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n") throw("bad recovery") } // Make the deferproc for this d return again, // this time returning 1. The calling function will // jump to the standard return epilogue. gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched) }
粗略一看,似乎就是很简单的设置了一些值?但实际上设置的是编译器中伪寄存器的值,常常被用于维护上下文等。在这里我们需要结合 gopanic 方法一同观察 recovery 方法。它所使用的栈指针 sp 和程序计数器 pc 是由当前 defer 在调用流程中的 deferproc 传递下来的,因此实际上最后是通过 gogo 方法跳回了 deferproc 方法。另外我们注意到:
gp.sched.ret = 1
在底层中程序将 gp.sched.ret 设置为了 1,也就是没有实际调用 deferproc 方法,直接修改了其返回值。意味着默认它已经处理完成。直接转移到 deferproc 方法的下一条指令去。至此为止,异常状态的流转控制就已经结束了。接下来就是继续走 defer 的流程了.
当然如果所有的 defer 都没有指明显式的 recover,那么这时候则直接在运行时抛出 panic 信息:
// 消耗完所有的 defer 调用,保守地进行 panic // 因为在冻结之后调用任意用户代码是不安全的,所以我们调用 preprintpanics 来调用 // 所有必要的 Error 和 String 方法来在 startpanic 之前准备 panic 字符串。 preprintpanics(gp._panic) fatalpanic(gp._panic) // 不应该返回 *(*int)(nil) = 0 // 无法触及
从 panic 和 recover 这对关键字的实现上可以看出,可恢复的 panic 必须要 recover 的配合。 而且,这个 recover 必须位于同一 goroutine 的直接调用链上(例如,如果 A 依次调用了 B 和 C,而 B 包含了 recover,而 C 发生了 panic,则这时 B 的 panic 无法恢复 C 的 panic; 又例如 A 调用了 B 而 B 又调用了 C,那么 C 发生 panic 时,如果 A 要求了 recover 则仍然可以恢复), 否则无法对 panic 进行恢复。
当一个 panic 被恢复后,调度并因此中断,会重新进入调度循环,进而继续执行 recover 后面的代码, 包括比 recover 更早的 defer(因为已经执行过得 defer 已经被释放,而尚未执行的 defer 仍在 goroutine 的 defer 链表中), 或者 recover 所在函数的调用方。
热门文章
- 动物医院实践心得体会总结报告范文(动物医院实践心得体会总结报告范文怎么写)
- 猫三联打几针多少钱(猫三联打三针还是两针)
- 关于宠物猫疫苗的作文(关于宠物猫疫苗的作文怎么写)
- 被猫咬后周围肿起来一大块(被猫咬后周围肿起来一大块整个胳膊都疼)
- 动物疫苗的作用有哪些方面呢 动物疫苗的作用有哪些方面呢英语
- Nodejs对数据库的增删改查
- 「2月19日」最高速度19.5M/S,2025年V2ray/SSR/Shadowrocket/Clash每天更新免费订阅地址分享
- 重庆宠物领养中心电话号码是多少啊(重庆最大免费宠物领养平台)
- ELK日志保留7天-索引生命周期策略
- 合肥宠物寄养中心(合肥宠物寄养中心在哪里)