内容简介:作为一个 gophper,我相信你对于输出结果:请思考一下,为什么执行
深入理解 Go panic and recover
作为一个 gophper,我相信你对于 panic 和 recover 肯定不陌生,但是你有没有想过。当我们执行了这两条语句之后。底层到底发生了什么事呢?前几天和同事刚好聊到相关的话题,发现其实大家对这块理解还是比较模糊的。希望这篇文章能够从更深入的角度告诉你为什么,它到底做了什么事?
思考
一、为什么会中止运行
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 后会导致应用程序运行中止?(而不是单单说执行了 panic 所以就结束了这么含糊)
二、为什么不会中止运行
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 行不
上面问题二是 defer + recover 组合,那我去掉 defer 是不是也可以呢?如下:
func main() {
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:10 +0xa1 exit status 2
竟然不行,啊呀毕竟入门教程都写的 defer + recover 组合 “万能” 捕获。但是为什么呢。去掉 defer 后为什么就无法捕获了?
请思考一下,为什么需要设置 defer 后 recover 才能起作用?
同时你还需要仔细想想,我们设置 defer + recover 组合后就能无忧无虑了吗,各种 “乱” 写了吗?
四、为什么起个 goroutine 就不行
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 调用情况,涉及的字段如下:
- argp:指向
defer延迟调用的参数的指针 - arg:
panic的原因,也就是调用panic时传入的参数 - link:指向上一个调用的
_panic - recovered:
panic是否已经被处理,也就是是否被recover - aborted:
panic是否被中止
另外通过查看 link 字段,可得知其是一个链表的数据结构,如下图:
恐慌 panic
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 进行检测并处理,最后调用退出命令中止应用程序
无法恢复的恐慌 fatalpanic
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) 教做人。因此在写代码时你应该要相对注意些,“恐慌” 是存在无法恢复的场景的
恢复 recover
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) ...
通过分析底层调用,可得知主要是如下几个方法:
- runtime.deferproc
- runtime.gopanic
- runtime.deferreturn
- runtime.gorecover
在上小节中,我们讲述了简单的流程, 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 有一定的关联性,因此需要有一定的基础知识。若刚刚看的时候这部分不理解,学习后可以再读一遍加深印象
在最后,现在的你可以回答这几个思考题了吗?说出来了才是真的懂 :)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java Servlet & JSP Cookbook
Bruce W. Perry / O'Reilly Media / 2003-12-1 / USD 49.99
With literally hundreds of examples and thousands of lines of code, the Java Servlet and JSP Cookbook yields tips and techniques that any Java web developer who uses JavaServer Pages or servlets will ......一起来看看 《Java Servlet & JSP Cookbook》 这本书的介绍吧!