今天咱们讲讲golang中panic异常,以及recover对异常的捕获,因为panic、recover、defer之间很是亲密,因此今天就放在一块儿讲解,这里会涉及到一些defer的知识,有兴趣能够看个人另外一篇关于defer的文章 Golang中defer的实现原理.html
Panic异常
Go的类型系统会在编译时捕获不少错误,但有些错误只能在运行时检查,如数组访问越界、 空指针引用等。这些运行时错误会引发painc异常。
通常而言,当panic异常发生时,程序会中断运行,并当即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。
不是全部的panic异常都来自运行时,直接调用内置的panic函数也会引起panic异常
接下来,咱们经过其汇编码尝试找出内置函数panic()的底层实现。
git
注意:我会把源码中每一个方法的做用都注释出来,能够参考注释进行理解。golang
先编写一段简单的代码,并保存在panic.go文件中web
func main() { panic("err") }
而后使用如下命令编译代码:编程
go tool compile -S panic.go
0x0024 00036 (panic.go:10) PCDATA $2, $1 0x0024 00036 (panic.go:10) PCDATA $0, $0 0x0024 00036 (panic.go:10) LEAQ type.string(SB), AX 0x002b 00043 (panic.go:10) PCDATA $2, $0 0x002b 00043 (panic.go:10) MOVQ AX, (SP) 0x002f 00047 (panic.go:10) PCDATA $2, $1 0x002f 00047 (panic.go:10) LEAQ "".statictmp_0(SB), AX 0x0036 00054 (panic.go:10) PCDATA $2, $0 0x0036 00054 (panic.go:10) MOVQ AX, 8(SP) 0x003b 00059 (panic.go:10) CALL runtime.gopanic(SB)
咱们能够看到panic()函数调用被替换成了runtime.gopanic()函数
看函数以前,咱们先来看一下panic的结构体
数组
runtime\runtime2.go:_panic安全
type _panic struct { argp unsafe.Pointer // 指向在panic下运行的defer的参数的指针 arg interface{ } // panic的参数 link *_panic // 连接到更早的panic,新panic添加到表头 recovered bool // 该panic是否被recover aborted bool // 该panic是否强制退出 }
接着,咱们再来分析runtime.gopanic()函数服务器
runtime\panic.go函数
func gopanic(e interface{ }) { //获取当前goroutine gp := getg() ... //生成一个新的panic结构 var p _panic p.arg = e //指向更早的panic p.link = gp._panic //绑定到goroutine gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) atomic.Xadd(&runningPanicDefers, 1) //循环goroutine中的defer链表 for { d := gp._defer if d == nil { break } //若是defer已经被调用 //若是该defer已经由较早的panic或者Goexit使用(表示引起了新的panic) //则从链表中去除这个panic,以前的panic或Goexit将不会继续运行。 if d.started { if d._panic != nil { d._panic.aborted = true } d._panic = nil d.fn = nil gp._defer = d.link //释放该defer freedefer(d) //跳过循环,继续下一个defer continue } // 将defer标记已调用,但保留在列表中 //这样 traceback 在栈增加或者 GC 的时候,可以找到并更新 defer 的参数栈帧 // 并用 reflectcall 执行 d.fn d.started = true //记录在 defer 中发生的 panic //若是在 defer 的函数调用过程当中又发生了新的 panic,那个 panic 会在链表中找到 d // 而后标记 d._panic(指向当前的 panic) 为 aborted 状态。 d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) p.argp = unsafe.Pointer(getargp(0)) //执行defer后面的fn,若是有recover()函数会执行recover reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) p.argp = nil // reflectcall 并无 panic,移除 d if gp._defer != d { throw("bad defer entry in panic") } //清空defer d._panic = nil d.fn = nil //下一个defer gp._defer = d.link // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic //GC() //defer语句下一条语句的地址 pc := d.pc //获取rsp寄存器的值的指针 //必须是指针,以便在堆栈复制期间进行调整 sp := unsafe.Pointer(d.sp) //释放defer freedefer(d) //若是panic被recover //会在gorecove 函数中已经修改成 true ,等会咱们在讲 if p.recovered { //统计 atomic.Xadd(&runningPanicDefers, -1) //下一个panic gp._panic = p.link // 已标记已停止的panic,q且保留在g.panic列表中。 //从列表中删除它们。 for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } //处理完全部panic if gp._panic == nil { // 必须用信号完成 gp.sig = 0 } // Pass information about recovering frame to recovery. //将有关恢复帧的信息传递给recovery函数 //经过以前传入的 sp 和 pc 恢复 gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") // mcall should not return } } // ran out of deferred calls - old-school panic now // Because it is unsafe to call arbitrary user code after freezing // the world, we call preprintpanics to invoke all necessary Error // and String methods to prepare the panic strings before startpanic. preprintpanics(gp._panic) //致命错误,终止程序 fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached }
接着,咱们再来看看它是如何经过recovery函数回复的ui
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") } //让这个 defer 结构体的 deferproc 位置的调用从新返回 // 此次将返回值修改成 1 gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 //直接跳回到deferreturn那里去 gogo(&gp.sched) }
咱们再来总结一下整个流程:
- 先建立一个_panic结构体,加载到链表的表头
- 遍历当前goroutine的defer链表,
- 若是defer被标记为已调用,跳出当前循环,进入下一个defer;
- 不然,将当前defer标记为已调用,同时执行defer后面的函数,若是有recover,则会经过以前建立defer时传进来的deferproc 的下一条汇编指令的地址(pc),以及函数调用栈栈顶的位置(sp)返回到deferreturn的位置上去,不然,直接退出程序
Recover捕获异常
一般来讲,不该该对panic异常作任何处理,但有时,也许咱们能够从异常中恢复,至少咱们 能够在程序崩溃前,作一些操做。好比说:当web服务器遇到不可预料的严重问题时,在崩溃前应该将全部的链接关闭,服务器甚至能够将异常信息反馈到客户端,帮助调试。
若是在defer函数中调用了内置函数recover,而且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。致使panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
recover函数的使用
1.recover必须与defer配合使用
func main() { defer func() { recover() }() panic("err") }
相似于下面这种状况是不能够的:
func main() { recover() panic("触发异常") }
2.必须在defer函数中直接调用recover,不能进行封装或者嵌套
func main() { defer func() { if r := MyRecover(); r != nil { fmt.Println(r) } }() panic("err") } func MyRecover() interface{ } { fmt.Println("recover") return recover() }
一样,在defer中嵌套也不能够
func main() { defer func() { defer func() { if r := recover(); r != nil { fmt.Println(r) } }() }() panic("err") }
若是咱们直接在 defer 语句中调用 MyRecover 函数又能够正常工做了:
func main() { //正常捕获 defer MyRecover() panic("err") } func MyRecover() interface{ } { fmt.Println("recover") return recover() }
可是,若是 defer 语句直接调用 recover 函数,依然不能正常捕获异常:
func main() { // 没法捕获异常 defer recover() panic("err") }
必需要和有异常的栈帧只隔一个栈帧, recover 函数才能正常捕获异常。换言之, recover 函数捕获的是祖父一级调用函数栈帧的异常(恰好能够跨越一层 defer 函数)!
同时,为了不不加区分的panic被恢复,可能致使系统漏洞的问题,最安全的作饭,就是对不一样的错误类型分别处理
recover函数的原理
接下来,咱们经过底层源码来看看它是如何作到这些限制的:
runtime\panic.go
func gorecover(argp uintptr) interface{ } { gp := getg() p := gp._panic //必须存在panic //非runtime.Goexit(); //panic还未被恢复 //argp == uintptr(p.argp) //p.argp是最顶层的延迟函数调用的参数指针,argp是调用recover函数的参数地址,一般是defer函数的参数地址 //若是二者相等,说明能够被恢复,这也是为何recover必须跟在defer后面且recover 函数捕获的是祖父一级调用函数栈帧的异常的缘由 if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) { //将recovered 标志改成true p.recovered = true return p.arg } return nil }
gorecover函数比较简单,就是将recovered设为true,说明已经defer后面的函数包含recover
总结
- recover函数在defer函数中
- recover函数被defer函数直接调用
- 若是包含多个defer函数,前面的defer经过recover()消除panic后,函数中剩余的defer仍然会执行,但不能再次recover()
- 连续调用panic,仅最后一个会被recover捕获