共计 3174 个字符,预计需要花费 8 分钟才能阅读完成。
panic 和 recover 也是罕用的关键字,这两个关键字与上一篇提到的 defer 分割很严密。用一句话总结就是:调用 panic 后会立即进行执行以后函数的残余代码,并在以后 Goroutine 中递归执行调用方的 defer;而 recover 能够停止 panic 造成的程序解体,不过它只能在 defer 中发挥作用。
panic
panic 是一个内置函数,承受一个任意类型的参数,参数将在程序解体时打印进去,如果被 recover 复原的话,该参数也是 recover 的返回值。panic 能够由程序员显式触发,运行时遇到意料之外的谬误如内存越界时也会触发。
在上一篇中咱们晓得每个 Goroutine 都保护了一个 _defer 链表(非凋谢编码状况下),执行过程中每遇到一个 defer 关键字都会创立一个 _defer 实例插入链表,函数退出时一次取出这些 _defer 实例并执行。panic 产生时,实际上是触发了函数退出,也即把执行流程转向了 _defer 链表。
panic 的执行过程中有几点须要明确:
- panic 会递归执行以后 Goroutine 中所有的 defer,解决实现后退出;
- panic 不会解决其余 Goroutine 中的 defer;
- panic 容许在 defer 中屡次调用,程序会终止以后 defer 的执行,持续之前的流程。
数据结构
panic 关键字在 Go 语言中是由数据结构 runtime._panic 示意的。每当咱们调用 panic 都会创立一个如下所示的数据结构:
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
goexit bool
}
- argp 是指向 defer 函数参数的指针;
- arg 是调用 panic 时传入的参数;
- link 指向前一个_panic 构造;
- recovered 示意以后 _panic 是否被 recover 复原;
- aborted 示意以后的 _panic 是否被终止;
- goexit 示意以后 _panic 是否是由 runtime.Goexit 产生的。
_panic 链表与 _defer 链表一样,都是保留在 Goroutine 的数据结构中:
type g struct {
// ...
_panic *_panic
_defer *_defer
// ...
}
执行过程
编译器会将关键字 panic 转换成 runtime.gopanic 函数,咱们来看一下它的外围代码:
func gopanic(e interface{}) {gp := getg()
...
var p _panic // 创立新的 _panic 构造
p.arg = e // 存储 panic 的参数
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 这两行是将新构造插入到以后 Goroutine 的 panic 链表头部
for {
d := gp._defer // 开始遍历 _defer 链表
if d == nil {break}
// 嵌套 panic 的情景
if d.started {
if d._panic != nil {d._panic.aborted = true // 标记之前 _defer 中的 _panic 为已终止}
// 从链表中删除本 defer
d._panic = nil
if !d.openDefer {
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
}
d.started = true // 标记 defer 曾经开始执行
d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 标记触发 defer 的 _panic
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) // 执行 defer 函数,省略对凋谢编码 _defer 的额定解决
d._panic = nil
d.fn = nil
gp._defer = d.link
pc := d.pc
sp := unsafe.Pointer(d.sp)
freedefer(d)
// 如果被 recover 复原的话,解决上面的逻辑
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") // mcall should not return
}
}
fatalpanic(gp._panic) // 终止整个程序
*(*int)(nil) = 0
}
该函数的执行过程蕴含以下几个步骤:
- 创立新的 runtime._panic 并增加到所在 Goroutine 的 _panic 链表的最后面;
- 判断是否是嵌套 panic 的情景,进行相干标记和解决;
- 一直从以后 Goroutine 的 _defer 链表中获取 _defer 并调用 runtime.reflectcall 运行提早调用函数;
- 调用 runtime.fatalpanic 停止整个程序。
recover
recover 也是一个内置函数,用于打消 panic 并使程序恢复正常。recover 的执行过程也有几点须要明确:
- recover 的返回值就是打消的 panic 的参数;
- recover 必须间接位于 defer 函数内(不能呈现在另一个嵌套函数中)能力失效;
- recover 胜利解决异样后,函数不会持续解决 panic 之后的逻辑,会间接返回,对于匿名返回值将返回相应的零值。
执行过程
编译器会将关键字 recover 转换成 runtime.gorecover:
func gorecover(argp uintptr) interface{} {gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
函数的实现很简略,获取以后 Goroutine 中的 _panic 实例,在符合条件的状况下将 _panic 实例的 recovered 状态标记为 true,而后返回 panic 函数的参数。
咱们来看一下 recover 的几个失效条件:
- p != nil:必须存在 panic;
- !p.goexit:非 runtime.Goexit();
- !p.recovered:还未被复原;
- argp == uintptr(p.argp):recover 必须在 defer 中间接调用。
首先,必须存在 panic,runtime.Goexit() 产生的 panic 无奈被复原,这些没什么好说的。假如函数蕴含多个 defer,后面的 defer 通过 recover 打消 panic 后,残余 defer 中的 recover 不能再次复原。
有一点会让人感到纳闷,recover 函数没有参数,为什么 gorecover 函数却有参数?这正是为了限度 recover 必须在 defer 中被间接调用。gorecover 函数的参数为调用 recover 函数的参数地址,_panic 构造中保留了以后 defer 函数的参数地址,如果二者统一,阐明 recover 是在 defer 中被间接调用。示例如下:
func test() {defer func() { // func A
func() { // func B
// gorecover 的参数 argp 为 B 的参数地址,p.argp 为 A 的参数的指针
// argp != p.argp,无奈复原
if err := recover(); err != nil {fmt.Println(err)
}
}()}()}