Panic
Go 的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。
由于panic会引起程序的崩溃,因此panic一般用于严重错误,如该错误的作用范围影响到整个系统的运行。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。
defer
defer是一个面向编译器的声明,他会让编译器做两件事:
编译器会将defer声明编译为runtime.deferproc(fn),这样运行时,会调用runtime.deferproc,在deferproc中将所有defer挂到goroutine的defer链上;
编译器会在函数return之前(注意,是return之前,而不是return xxx之前,后者不是一条原子指令),增加runtime.deferreturn调用;这样运行时,开始处理前面挂在defer链上的所有defer。
Recover
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。
不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。
有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。
基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常。
总结
- panic() 执行后,后续语句不再执行,会先调用当前协程的 defer 链表。
- 如果某个 goroutine 的 defer 没有 recover,会终止整个程序(exit(2)),不仅仅是终止当前 goroutine 。
- 如发现 defer 函数包含 recover, 则会运行 recovery 函数,recovery 会跳转到 deferreturn 。
- panic 被 recover 后,会影响到当前函数中的后续语句的执行,但不影响当前 goroutine 的继续执行。
- recover() 的作用是捕获异常之后让程序正常往下执行而不会退出。
- recover() 必须写在defer语块中才能生效。
- recover() 的作用范围仅限于当前的所属 go routine。发生 panic 时只会执行当前协程中的defer函数,其它协程里面的 defer 不会执行。
- 如果要查找堆栈跟踪,请使用在 Debug 程序包下定义的 PrintStack 函数。
- 既然你要使用 panic,那为什么要 recover ?你的期望是什么?如果不希望 go die 为什么要用 panic ?
- 如果实在每个 panic 都想捕获,可以考虑把 panic 这样的事件通知给其他 goroutine 处理。