panic
panic
panic
是go的内置函数,当遇到不可恢复的错误时,程序往往就会抛出panic
,比如常见的空指针访问
func main() {
var a *int
*a = 1
}
运行上面这段代码,程序就会抛出如下的panic
,然后程序就会停止。
panic: runtime error: invalid memory address or nil pointer dereference
在一些情况下,我们也会手动调用panic
函数来让程序退出,从而避免更严重的后果。平时也会用另一个内置函数recover
来捕获panic
,并配合defer
使用。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}
为什么recover
函数一定要在defer
里面使用,recover
做了些什么工作,这些问题都会在下面的内容得到解答。
结构
panic
在运行时也有对应的结构进行表示,那就是runtime._panic
,其结构并不复杂,如下。
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg any // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}
它的结构与defer
非常类似,
link
指向下一个_panic
结构,pc
和sp
指向调用函数的执行现场便于日后恢复,arg
就是panic
函数的参数,argp
指向defer
的参数,panic
发生后便会触发defer
的执行aborted
表示其是否被强制停止
panic
跟defer
一样,也是以链表的形式存在于协程中
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
恐慌
无论是我们主动调用panic
函数,抑或是程序发生的panic
,最终都会走入runtime.gopanic
函数中
func gopanic(e any)
在开始时,首先会检测参数是否为nil
,如果是nil
的话就会new一个runtime.PanicNilError
类型的错误
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}
然后将当前的panic
加入协程的链表头部
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
随后进入for
循环开始逐个处理当前协程的defer
链表
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
}
如果当前的defer
已经被其它的panic
触发了,即_defer.started == true
,那么较早的panic
将不会执行。然后再执行defer
对应的函数
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
执行完后回收当前的defer
结构,继续执行下一个defer
,当执行完全部的defer
结构后且期间没有被恢复,就会进入runtime.fatalpanic
函数,该函数是unrecoverable
即不可恢复的
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
runningPanicDefers.Add(-1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}
在这期间会让printpanics
打印panic
的信息,我们通常看到的调用栈信息就是由它输出的,最后由runtime.exit
函数通过系统调用_ExitProcess
退出程序。
恢复
通过调用内置函数recover
,编译期间会变为对runtime.gorecover
函数的调用
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
它的实现非常简单,只干了p.recovered = true
这么一件事,而真正负责处理恢复逻辑的代码实际上在gopanic
函数里
for {
...
d.fn()
...
if p.recovered {
...
}
}
恢复的逻辑在defer
执行后,到这里也就明白了为什么recover
函数只能在defer
中使用,如果在defer
之外使用recover
函数的话gp._panic
就等于nil
,自然p.recovered
就不会被设置为true
,那么在gopanic
函数中也就不会走到恢复这部分逻辑里面来。
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")
}
恢复时会清理链表中那些已经被强制停止的panic
,然后进入runtime.recovery
函数中,由runtime.gogo
回到用户函数的正常逻辑流程中
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
然后有一个重点需要注意的是这行代码
gp.sched.ret = 1
它将ret
值设置成为了1,从runtime.deferproc
的函数注释中可以看代码下面这些内容
func deferproc(fn func()) {
...
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
}
编译器生成的中间代码会检查该值是否为1,如果是的话就会直接执行runtime.deferreturn
函数,通常该函数只有在函数返回之前才会执行,这也说明了为什么recover
过后函数会直接返回。