做为一个 gophper,我相信你对于 panic
和 recover
确定不陌生,可是你有没有想过。当咱们执行了这两条语句以后。底层到底发生了什么事呢?前几天和同事恰好聊到相关的话题,发现其实你们对这块理解仍是比较模糊的。但愿这篇文章可以从更深刻的角度告诉你为何,它到底作了什么事?php
原文地址:深刻理解 Go panic and recovergit
func main() { panic("EDDYCJY.") }
输出结果:github
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39 exit status 2
请思考一下,为何执行 panic
后会致使应用程序运行停止?(而不是单单说执行了 panic
因此就结束了这么含糊)golang
func main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("EDDYCJY.") }
输出结果:数据结构
$ go run main.go 2019/05/11 23:39:47 recover: EDDYCJY.
请思考一下,为何加上 defer
+ recover
组合就能够保护应用程序?函数
上面问题二是 defer
+ recover
组合,那我去掉 defer
是否是也能够呢?以下:学习
func main() { if err := recover(); err != nil { log.Printf("recover: %v", err) } panic("EDDYCJY.") }
输出结果:ui
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10 +0xa1 exit status 2
居然不行,啊呀毕竟入门教程都写的 defer
+ recover
组合 “万能” 捕获。可是为何呢。去掉 defer
后为何就没法捕获了?atom
请思考一下,为何须要设置 defer
后 recover
才能起做用?spa
同时你还须要仔细想一想,咱们设置 defer
+ recover
组合后就能无忧无虑了吗,各类 “乱” 写了吗?
func main() { go func() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() }() panic("EDDYCJY.") }
输出结果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14 +0x51 exit status 2
请思考一下,为何新起了一个 Goroutine
就没法捕获到异常了?到底发生了什么事...
接下来咱们将带着上述 4+1 个小思考题,开始对源码的剖析和分析,尝试从阅读源码中找到思考题的答案和更多为何
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool }
在 panic
中是使用 _panic
做为其基础单元的,每执行一次 panic
语句,都会建立一个 _panic
。它包含了一些基础的字段用于存储当前的 panic
调用状况,涉及的字段以下:
defer
延迟调用的参数的指针panic
的缘由,也就是调用 panic
时传入的参数_panic
panic
是否已经被处理,也就是是否被 recover
panic
是否被停止另外经过查看 link
字段,可得知其是一个链表的数据结构,以下图:
func main() { panic("EDDYCJY.") }
输出结果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39 exit status 2
咱们去反查一下 panic
处理具体逻辑的地方在哪,以下:
$ go tool compile -S main.go "".main STEXT size=66 args=0x0 locals=0x18 0x0000 00000 (main.go:23) TEXT "".main(SB), ABIInternal, $24-0 0x0000 00000 (main.go:23) MOVQ (TLS), CX 0x0009 00009 (main.go:23) CMPQ SP, 16(CX) ... 0x002f 00047 (main.go:24) PCDATA $2, $0 0x002f 00047 (main.go:24) MOVQ AX, 8(SP) 0x0034 00052 (main.go:24) CALL runtime.gopanic(SB)
显然汇编代码直指内部实现是 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 } // defer... ... 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 // recover... if p.recovered { ... 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
用做后续的操做Goroutine
上挂载的 _defer
(数据结构也是链表)defer
调用,则调用 reflectcall
方法去执行先前 defer
中延迟执行的代码,若在执行过程当中须要运行 recover
将会调用 gorecover
方法preprintpanics
方法打印出所涉及的 panic
消息fatalpanic
停止应用程序,实际是执行 exit(2)
进行最终退出行为的经过对上述代码的执行分析,可得知 panic
方法实际上就是处理当前 Goroutine(g)
上所挂载的 ._panic
链表(因此没法对其余 Goroutine
的异常事件响应),而后对其所属的 defer
链表和 recover
进行检测并处理,最后调用退出命令停止应用程序
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() var docrash bool systemstack(func() { if startpanic_m() && msgs != nil { ... printpanics(msgs) } docrash = dopanic_m(gp, pc, sp) }) systemstack(func() { exit(2) }) *(*int)(nil) = 0 }
咱们看到在异常处理的最后会执行该方法,彷佛它承担了全部收尾工做。实际呢,它是在最后对程序执行 exit
指令来达到停止运行的做用,但在结束前它会经过 printpanics
递归输出全部的异常消息及参数。代码以下:
func printpanics(p *_panic) { if p.link != nil { printpanics(p.link) print("\t") } print("panic: ") printany(p.arg) if p.recovered { print(" [recovered]") } print("\n") }
因此不要觉得全部的异常都可以被 recover
到,实际上像 fatal error
和 runtime.throw
都是没法被 recover
到的,甚至是 oom 也是直接停止程序的,也有反手就给你来个 exit(2)
教作人。所以在写代码时你应该要相对注意些,“恐慌” 是存在没法恢复的场景的
func main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("EDDYCJY.") }
输出结果:
$ go run main.go 2019/05/11 23:39:47 recover: EDDYCJY.
和预期一致,成功捕获到了异常。可是 recover
是怎么恢复 panic
的呢?再看看汇编代码,以下:
$ go tool compile -S main.go "".main STEXT size=110 args=0x0 locals=0x18 0x0000 00000 (main.go:5) TEXT "".main(SB), ABIInternal, $24-0 ... 0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX 0x002b 00043 (main.go:6) PCDATA $2, $0 0x002b 00043 (main.go:6) MOVQ AX, 8(SP) 0x0030 00048 (main.go:6) CALL runtime.deferproc(SB) ... 0x0050 00080 (main.go:12) CALL runtime.gopanic(SB) 0x0055 00085 (main.go:12) UNDEF 0x0057 00087 (main.go:6) XCHGL AX, AX 0x0058 00088 (main.go:6) CALL runtime.deferreturn(SB) ... 0x0022 00034 (main.go:7) MOVQ AX, (SP) 0x0026 00038 (main.go:7) CALL runtime.gorecover(SB) 0x002b 00043 (main.go:7) PCDATA $2, $1 0x002b 00043 (main.go:7) MOVQ 16(SP), AX 0x0030 00048 (main.go:7) MOVQ 8(SP), CX ... 0x0056 00086 (main.go:8) LEAQ go.string."recover: %v"(SB), AX ... 0x0086 00134 (main.go:8) CALL log.Printf(SB) ...
经过分析底层调用,可得知主要是以下几个方法:
在上小节中,咱们讲述了简单的流程,gopanic
方法会调用当前 Goroutine
下的 defer
链表,若 reflectcall
执行中遇到 recover
就会调用 gorecover
进行处理,该方法代码以下:
func gorecover(argp uintptr) interface{} { 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) { sp := gp.sigcode0 pc := gp.sigcode1 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") } 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
的流程了
为了验证这个想法,咱们能够看一下核心的跳转方法 gogo
,代码以下:
// void gogo(Gobuf*) // restore state from Gobuf; longjmp TEXT runtime·gogo(SB),NOSPLIT,$8-4 MOVW buf+0(FP), R1 MOVW gobuf_g(R1), R0 BL setg<>(SB) MOVW gobuf_sp(R1), R13 // restore SP==R13 MOVW gobuf_lr(R1), LR MOVW gobuf_ret(R1), R0 MOVW gobuf_ctxt(R1), R7 MOVW $0, R11 MOVW R11, gobuf_sp(R1) // clear to help garbage collector MOVW R11, gobuf_ret(R1) MOVW R11, gobuf_lr(R1) MOVW R11, gobuf_ctxt(R1) MOVW gobuf_pc(R1), R11 CMP R11, R11 // set condition codes for == test, needed by stack split B (R11)
经过查看代码可得知其主要做用是从 Gobuf
恢复状态。简单来说就是将寄存器的值修改成对应 Goroutine(g)
的值,而在文中讲了不少次的 Gobuf
,以下:
type gobuf struct { sp uintptr pc uintptr g guintptr ctxt unsafe.Pointer ret sys.Uintreg lr uintptr bp uintptr }
讲道理,其实它存储的就是 Goroutine
切换上下文时所须要的一些东西
const( OPANIC // panic(Left) ORECOVER // recover() ... ) ... func walkexpr(n *Node, init *Nodes) *Node { ... switch n.Op { default: Dump("walk", n) Fatalf("walkexpr: switch 1 unknown op %+S", n) case ONONAME, OINDREGSP, OEMPTY, OGETG: case OTYPE, ONAME, OLITERAL: ... case OPANIC: n = mkcall("gopanic", nil, init, n.Left) case ORECOVER: n = mkcall("gorecover", n.Type, init, nod(OADDR, nodfp, nil)) ... }
实际上在调用 panic
和 recover
关键字时,是在编译阶段先转换为相应的 OPCODE 后,再由编译器转换为对应的运行时方法。并非你所想像那样一步到位,有兴趣的小伙伴能够研究一下
本文主要针对 panic
和 recover
关键字进行了深刻源码的剖析,而开头的 4+1 个思考题,就是但愿您可以带着疑问去学习,达到事半功倍的功效
另外本文和 defer
有必定的关联性,所以须要有必定的基础知识。若刚刚看的时候这部分不理解,学习后能够再读一遍加深印象
在最后,如今的你能够回答这几个思考题了吗?说出来了才是真的懂 :)