谈谈 panic 和 recover 的原理
source link: http://draveness.me/golang-panic-recover?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
这一节中我们将介绍 Go 语言中两个经常成对出现的关键字 panic
和 recover
的实现原理,我们在上一节关注的defer 与这里介绍的两个关键字其实也有着比较大的关系,我们会在剩下的部分展开介绍相关的内容,没有阅读上一节 的读者还是需要补充一下相关知识,这样才能更好地了解 panic
和 recover
关键字的原理。
在具体介绍和分析 Go 语言中的 panic
和 recover
的实现原理之前,我们首先需要对它们有一些基本的了解; panic
和 recover
两个关键字其实都是 Go 语言中的内置函数, panic
能够改变程序的控制流,当一个函数调用执行 panic
时,它会立刻停止执行函数中其他的代码,而是会运行其中的 defer
函数,执行成功后会返回到调用方。
对于上层调用方来说,调用导致 panic
的函数其实与直接调用 panic
类似,所以也会执行所有的 defer
函数并返回到它的调用方,这个过程会一直进行直到当前 Goroutine 的调用栈中不包含任何的函数,这时整个程序才会崩溃,这个『恐慌过程』不仅会被显式的调用触发,还会由于运行期间发生错误而触发。
然而 panic
导致的『恐慌』状态其实可以被 defer 中的 recover
中止, recover
是一个只在 defer
中能够发挥作用的函数,在正常的控制流程中,调用 recover
会直接返回 nil
并且没有任何的作用,但是如果当前的 Goroutine 发生了『恐慌』, recover
其实就能够捕获到 panic
抛出的错误并阻止『恐慌』的继续传播。
概述这一小节的内容,大部分直接来自于 Go 语言的博客 Defer, Panic, and Recover ,文章介绍了三种 Go 语言的常见关键字的常见使用场景。
常见使用
我们简单举两个例子简单了解一下 panic
和 recover
关键字的原理,先来看第一个例子:
func main() { defer println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(1 * time.Second) } // in goroutine // panic: // ...
当我们运行这段代码时,其实会发现 main
函数中的 defer
语句并没有执行,执行的其实只有 Goroutine 中的 defer
,这其实就印证了 Go 语言在发生 panic
时只会执行当前协程中的 defer
函数,这一点从上一节 的源代码中也有所体现。
另一个例子就不止涉及 panic
和 defer
关键字了,我们可以看一下 recover
是如何让当前函数重新『走向正轨』的:
func main() { defer fmt.Println("in main") defer func() { if err := recover(); err != nil { fmt.Println(err) } }() panic("unknown err") } // unknown err // in main
从这个例子中我们可以看到, recover
函数其实只是阻止了当前程序的崩溃,但是当前控制流中的其他 defer
函数还会正常执行。
在最后,我们需要知道的是可以在 defer
中连续多次调用 panic
函数,这是一个 Go 语言中 panic
比较有意思的现象:
func main() { defer fmt.Println("in main") defer func() { panic("panic again") }() panic("panic once") } // in main // panic: unknown err // panic: again // // goroutine 1 [running]: // main.main.func1() // ...
当我们运行上述代码时,从打印出的结果中可以看到当前的函数确实经历了两次 panic
,并且最外层的 defer
函数也能够正常执行
实现原理
既然已经介绍完了现象并且已经对 panic
和 recover
有了一定的了解,接下来我们就会从 Go 语言的源代码层面对上一节中谈到的现象一探究竟,这一节接下来的内容就是介绍这两个函数的实现原理了,作为 Go 语言中的关键字,我们还是会从编译期间和运行时两方面介绍它们。
panic
和 recover
关键字会在编译期间 被 Go 语言的编译器转换成 OPANIC
和 ORECOVER
类型的节点并进一步转换成 gopanic
和 gorecover
两个运行时的函数调用。
数据结构
panic
在 Golang 中其实是由一个数据结构表示的,每当我们调用一次 panic
函数都会创建一个如下所示的数据结构存储相关的信息:
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool }
-
argp
是指向defer
调用时参数的指针; -
arg
是调用panic
时传入的参数; -
link
指向了更早调用的_panic
结构; -
recovered
表示当前_panic
是否被recover
恢复; -
aborted
表示当前的panic
是否被强行终止;
从数据结构中的 link
字段我们就可以推测出以下的结论 — panic
函数可以被连续多次调用,它们之间通过 link
的关联形成一个链表。
首先了解一下没有被 recover
的 panic
函数是如何终止整个程序的,我们来看一下 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 } 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 d._panic = nil d.fn = nil gp._defer = d.link pc := d.pc sp := unsafe.Pointer(d.sp) freedefer(d) if p.recovered { // ... } } fatalpanic(gp._panic) *(*int)(nil) = 0 }
我们暂时省略了 recover
相关的代码,省略后的 gopanic
函数执行过程包含以下几个步骤:
- 获取当前
panic
调用所在的 Goroutine 协程; - 创建并初始化一个
_panic
结构体; - 从当前 Goroutine 中的链表获取一个
_defer
结构体; - 如果当前
_defer
存在,调用reflectcall
执行_defer
中的代码; - 将下一位的
_defer
结构设置到 Goroutine 上并回到 3; - 调用
fatalpanic
中止整个程序;
fatalpanic
函数在中止整个程序之前可能就会通过 printpanics
打印出全部的 panic
消息以及调用时传入的参数:
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() var docrash bool systemstack(func() { if startpanic_m() && msgs != nil { atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs) } docrash = dopanic_m(gp, pc, sp) }) if docrash { crash() } systemstack(func() { exit(2) }) *(*int)(nil) = 0 // not reached }
在 fatalpanic
函数的最后会通过 exit
退出当前程序并返回错误码 2
,不同的操作系统其实对 exit
函数有着不同的实现,其实最终都执行了 exit
系统调用来退出程序。
到了这里我们已经掌握了 panic
退出程序的过程,但是一个 panic
的程序也可能会被 defer
中的关键字 recover
恢复,在这时我们就回到 recover
关键字对应函数 gorecover
的实现了:
func gorecover(argp uintptr) interface{} { p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
这个函数的实现其实非常简单,它其实就是会修改 panic
结构体的 recovered
字段,当前函数的调用其实都发生在 gopanic
期间,我们重新回顾一下这段方法的实现:
func gopanic(e interface{}) { // ... for { // reflectcall pc := d.pc sp := unsafe.Pointer(d.sp) // ... if p.recovered { 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") } } fatalpanic(gp._panic) *(*int)(nil) = 0 }
上述这段代码其实从 _defer
结构体中取出了程序计数器 pc
和栈指针 sp
并调用 recovery
方法进行调度,调度之前会准备好 sp
、 pc
以及函数的返回值:
func recovery(gp *g) { sp := gp.sigcode0 pc := gp.sigcode1 gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched) }
在defer 一节中我们曾经介绍过 deferproc
的实现,作为创建并初始化 _defer
结构体的函数,它会将 deferproc
函数开始位置对应的栈指针 sp
和程序计数器 pc
存储到 _defer
结构体中,这里的 gogo
函数其实就会跳回 deferproc
:
TEXT runtime·gogo(SB), NOSPLIT, $8-4 MOVL buf+0(FP), BX // gobuf MOVL gobuf_g(BX), DX MOVL 0(DX), CX // make sure g != nil get_tls(CX) MOVL DX, g(CX) MOVL gobuf_sp(BX), SP // restore SP MOVL gobuf_ret(BX), AX MOVL gobuf_ctxt(BX), DX MOVL $0, gobuf_sp(BX) // clear to help garbage collector MOVL $0, gobuf_ret(BX) MOVL $0, gobuf_ctxt(BX) MOVL gobuf_pc(BX), BX JMP BX
这里的调度其实会将 deferproc
函数的返回值设置成 1
,在这时编译器生成的代码就会帮助我们直接跳转到调用方函数 return
之前并进入 deferreturn
的执行过程,我们可以从 deferproc
的注释中简单了解这一过程:
func deferproc(siz int32, fn *funcval) { // ... // 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. }
跳转到 deferreturn
函数之后,程序其实就从 panic
的过程中跳出来恢复了正常的执行逻辑,而 gorecover
函数也从 _panic
结构体中取出了调用 panic
时传入的 arg
参数。
Go 语言中 panic
和 recover
的实现其实与defer 关键字的联系非常紧密,而分析程序的恐慌和恢复过程也比较棘手,不是特别容易理解。在文章的最后我们还是简单总结一下具体的实现原理:
- 在编译过程中会将
panic
和recover
分别转换成gopanic
和gorecover
函数,同时将defer
转换成deferproc
函数并在调用defer
的函数和方法末尾增加deferreturn
的指令; - 在运行过程中遇到
gopanic
方法时,会从当前 Goroutine 中取出_defer
的链表并通过reflectcall
调用用于收尾的函数; - 如果在
reflectcall
调用时遇到了gorecover
就会直接将当前的_panic.recovered
标记成true
并返回panic
传入的参数( 在这时recover
就能够获取到panic
的信息 );- 在这次调用结束之后,
gopanic
会从_defer
结构体中取出程序计数器pc
和栈指针sp
并调用recovery
方法进行恢复; -
recovery
会根据传入的pc
和sp
跳转到deferproc
函数; - 编译器自动生成的代码会发现
deferproc
的返回值不为0
,这时就会直接跳到deferreturn
函数中并恢复到正常的控制流程( 依次执行剩余的defer
并正常退出 );
- 在这次调用结束之后,
- 如果没有遇到
gorecover
就会依次遍历所有的_defer
结构,并在最后调用fatalpanic
中止程序、打印panic
参数并返回错误码2
;
整个过程涉及了一些 Go 语言底层相关的知识并且发生了非常多的跳转,相关的源代码也不是特别的直接,阅读起来也比较晦涩,不过还是对我们理解 Go 语言的错误处理机制有着比较大的帮助。
Reference
关于图片和转载
本作品采用 知识共享署名 4.0 国际许可协议
进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。
微信公众号
关于评论和留言
如果对本文的内容有疑问,请在下面的评论系统中留言,谢谢。Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK