panic
Go 运行时错误会引起 painc
异常。一般而言,当 panic
异常发生时,程序会中断运行,并立即执行在该 goroutine 中被延迟的函数(defer
机制)。随后,程序崩溃并输出日志信息。
由于 panic
会引起程序的崩溃,因此 panic
一般用于严重错误,如程序内部的逻辑不一致。但是对于大部分漏洞,我们应该使用 Go 提供的错误机制,而不是 panic
,尽量避免程序的崩溃。
panic
只会触发当前 goroutine 的延迟函数调用;recover
只有在defer
函数中调用才会生效;panic
允许在defer
中嵌套多次调用,只有最后一个会被recover
捕获;
panic 函数
panic
函数接受任何值作为参数。当某些不应该发生的场景发生时,就应该调用 panic
。
panic 详情中都有什么
panic: runtime error: index out of range
goroutine 1 [running]:
main.main()
/Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.go:5 +0x3d
exit status 2
第一行是 panic: runtime error: index out of range
。其中的 runtime error
的含义是,这是一个 runtime
代码包中抛出的 panic
。
goroutine 1 [running]
,它表示有一个 ID 为1的 goroutine 在此 panic
被引发的时候正在运行。这里的 ID 其实并不重要。
main.main()
表明了这个 goroutine 包装的 go 函数就是命令源码文件中的那个main
函数,也就是说这里的 goroutine 正是主 goroutine。
再下面的一行,指出的就是这个 goroutine 中的哪一行代码在此 panic 被引发时正在执行。含了此行代码在其所属的源码文件中的行数,以及这个源码文件的绝对路径。
+0x3d
代表的是:此行代码相对于其所属函数的入口程序计数偏移量。用处并不大。
exit status 2
表明我的这个程序是以退出状态码2结束运行的。在大多数操作系统中,只要退出状态码不是 0,都意味着程序运行的非正常结束。在 Go 语言中,因 panic 导致程序结束运行的退出状态码一般都会是 2。
从 panic 被引发到程序终止运行的大致过程是什么
- 此行代码所属函数的执行随即终止。
- 紧接着,控制权并不会在此有片刻停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。
这里的最外层函数指的是 go
函数,对于主 goroutine 来说就是 main
函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。
随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。
怎样让 panic 包含一个值,以及应该让它包含什么样的值
其实很简单,在调用 panic
函数时,把某个值作为参数传给该函数就可以了。panic
函数的唯一一个参数是空接口
(也就是interface{}
)类型的,所以从语法上讲,它可以接受任何类型的值。
但是,我们最好传入 error
类型的错误值,或者其他的可以被有效序列化的值。这里的“有效序列化”指的是,可以更易读地去表示形式转换。
recover 捕获异常
一般情况下,不能因为某个处理函数引发的 panic
异常,杀掉整个进程,可以使用 recover
函数恢复 panic
异常。
panic
时会调用 recover
,但是 recover
不能滥用,可能会引起资源泄漏或者其他问题。可以将 panic value
设置成特殊类型,来标识某个 panic
是否应该被恢复。recover
只能在 defer
修饰的函数中使用:
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil: // no panic
case bailout{}: // "expected" panic
err = fmt.Errorf("multiple title elements")
default:
panic(p) // unexpected panic; carry on panicking
}
}()
panic(bailout{})
}
上面的代码,deferred
函数调用 recover
,并检查 panic value
:
- 当
panic value
是bailout{}
类型时,deferred
函数生成一个error
返回给调用者。 - 当
panic value
是其他non-nil
值时,表示发生了未知的panic
异常。
正确使用 recover 函数
先调用 recover
函数,再调用 panic
函数会怎么样呢?
如果在调用 recover
函数时未发生 panic,那么该函数就不会做任何事情,并且只会返回一个 nil
。
defer
语句调用 recover
函数才是正确的打开方式。
要注意,尽量把 defer
语句写在函数体的开始处,因为在引发 panic 的语句之后的所有语句,都不会有任何执行机会。
panic
只有最后一个会被 recover
捕获。panic 和 recover 原理
panic
能够改变程序的控制流:
- 当一个函数中调用
panic
时会立刻停止执行该函数的其他代码,并在执行结束后在当前 goroutine 中递归执行调用方的延迟函数调用defer
; recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥任何作用;
defer
关键字对应的 runtime.deferproc
会将延迟调用函数与调用方所在 goroutine 进行关联。所以当程序发生崩溃时只会调用当前 goroutine 的延迟调用函数。
多次调用 panic
不会影响 defer
函数的正常执行。
数据结构 runtime._panic
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
pc uintptr
sp unsafe.Pointer
goexit bool
}
崩溃恢复
编译器会将关键字 recover
转换成 runtime.gorecover
:
func gorecover(argp uintptr) interface{} {
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
- 如果当前 goroutine 没有调用
panic
,那么该函数会直接返回nil
,这也是崩溃恢复在非defer
中调用会失效的原因。 - 修改
runtime._panic
结构体的recovered
字段。
runtime.gorecover
函数本身不包含恢复程序的逻辑,程序的恢复也是由 runtime.gopanic
函数负责的:
- 创建新的
runtime._panic
结构并添加到所在goroutine._panic
链表的最前面; - 在循环中不断从当前 goroutine 的
_defer
中链表获取runtime._defer
并调用runtime.reflectcall
运行延迟调用函数; - 调用
runtime.fatalpanic
中止整个程序;
func gopanic(e interface{}) {
gp := getg()
// ...
// 创建新的 runtime._panic 并添加到所在 goroutine 的 _panic 链表的最前面
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 在循环中不断从当前 goroutine 的 _defer 中链表获取 runtime._defer
// 并调用 runtime.reflectcall 运行延迟调用函数
for {
// 执行延迟调用函数,可能会设置 p.recovered = true
d := gp._defer
if d == nil {
break
}
d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 关联 defer 和 panic
// 运行延迟调用函数,如果碰到了 recover 函数,就会将 p.recovered 标记成 true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
// 从 _defer 中取出了程序计数器 pc 和栈指针 sp
pc := d.pc
sp := unsafe.Pointer(d.sp)
// ...
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 了从当前 goroutine 切换到 g0
// 调用 runtime.recovery 函数触发 goroutine 的调度
mcall(recovery)
throw("recovery failed")
}
}
// 调用 runtime.fatalpanic 中止整个程序,最终会调用 exit(2)
// 这是为什么连续调用 panic 只有最后一个会被 recover 捕获的原因
fatalpanic(gp._panic)
// ...
}
func recovery(gp *g) {
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
// 调用 defer 关键字时,调用时的栈指针 sp 和程序计数器 pc 就已经存储到了 runtime._defer 结构体中
// 这里的 runtime.gogo 函数会跳回 defer 关键字调用的位置
// gogo 函数从 g0 切换到调用 defer 的 goroutine
gogo(&gp.sched)
}
崩溃恢复流程
- 编译器会负责做转换关键字的工作;
- 将
panic
和recover
分别转换成runtime.gopanic
和runtime.gorecover
; - 将
defer
转换成deferproc
函数;
- 将
- 在调用
defer
的函数末尾插入deferreturn
函数; - 在运行过程中遇到
gopanic
方法时,会从 goroutine 的链表依次取出_defer
结构体并执行;- 如果调用延迟执行函数时遇到了
gorecover
就会将_panic.recovered
标记成true
并返回panic
的参数; - 在这次调用结束之后,
gopanic
会从_defer
结构体中取出程序计数器pc
和栈指针sp
并调用recovery
函数进行恢复程序; recovery
会根据传入的pc
和sp
跳转回deferproc
;- 编译器自动生成的代码会发现
deferproc
的返回值不为 0,这时会跳回deferreturn
并恢复到正常的执行流程;
- 如果调用延迟执行函数时遇到了
- 如果没有遇到
gorecover
就会依次遍历所有的_defer
结构,并在最后调用fatalpanic
中止程序、打印panic
的参数并返回错误码 2;