使用的go版本为 go1.21.2
首先我们写一个简单的panic调度与捕获代码
Go
package main
func main() {
defer func() {
recover()
}()
panic("panic test")
}
通过go build -gcflags -S main.go获取到对应的汇编代码
可以看到当我们调度panic时,Go的编译器会将这段代码翻译为CALL runtime.gopanic(SB)
我们先来看一下panic构造体的底层源码
panic源码与解读
Go
//代码位置 $GOROOT/src/runtime/runtime2.go L:1035
type _panic struct {
argp unsafe.Pointer // 指向在 panic 运行期间执行的延迟调用参数的指针,不可移动 - liblink 工具已知其位置
arg any // 参数
link *_panic // panic链表
pc uintptr // 返回到运行时的位置
sp unsafe.Pointer // 返回到运行时的栈指针位置
recovered bool // 是否已被恢复
aborted bool // 是否已被中止
goexit bool // 是否执行了 Goexit 函数
}
gopanic源码与解读
Go
//代码位置 $GOROOT/src/runtime/panic.go L:826
// 实现预声明函数 panic
func gopanic(e any) {
// 处理异常参数为 nil 的情况
if e == nil {
// 如果 debug.panicnil 不等于 1,将e设置为PanicNilError类型
//
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}
// 获取当前的G
gp := getg()
// 判断当前M上运行的G是不是当前G
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
// malloc过程中出现panic
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
// 禁止抢占的情况下执行 panic (!="" 保持当前G在这M运行)
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
// 当初M处于锁的状态
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
// 定义一个panic变量
var p _panic
p.arg = e //这个e 就是我们panic("xxxx") 里面写的东西
//将这个panic加入到G的_panic链表中去
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 增加运行panic延迟计数
runningPanicDefers.Add(1)
// 计算 getcallerpc/getcallersp,以避免扫描 gopanic 帧
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
for {
//逐步获取当前G中的defer调用
d := gp._defer
// 如果获取到的构造体为空,直接返回。
if d == nil {
break
}
// 如果当前_defer运行,将_defer从G的延迟链表移除,释放对应的_defer构造体资源,防止重复执行
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
if !d.openDefer {
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
}
// 标记当前_defer为运行状态
d.started = true
// 记录_defer的panic
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
done := true
if d.openDefer { //如果_defer使用了 open-coded defers(编码的延迟调用)
// 运行open-coded defer函数
done = runOpenDeferFrame(d) //如果当前栈下面没有其他延迟函数,则返回true
if done && !d._panic.recovered { //panic没有recover
addOneOpenDeferFrame(gp, 0, nil)
}
} else {//执行对应方法
//getargp返回其caller的保存callee参数的地址
p.argp = unsafe.Pointer(getargp())
d.fn()
}
p.argp = nil
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
pc := d.pc
sp := unsafe.Pointer(d.sp)
if done { //将_defer从G的延迟链表移除,释放对应的_defer构造体资源
d.fn = nil
gp._defer = d.link
freedefer(d)
}
if p.recovered { //panic已经恢复
gp._panic = p.link
if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
// A normal recover would bypass/abort the Goexit. Instead,
// we return to the processing loop of the Goexit.
gp.sigcode0 = uintptr(gp._panic.sp)
gp.sigcode1 = uintptr(gp._panic.pc)
mcall(recovery)
throw("bypassed recovery failed") // mcall should not return
}
runningPanicDefers.Add(-1)
// 从G中获取一个_defer构造体
d := gp._defer
var prev *_defer
if !done { //如果未执行完毕,跳过当前的帧直接执行下一个
prev = d
d = d.link
}
for d != nil {
if d.started { //如果启动退出循环
break
}
if d.openDefer { //如果使用了 open-coded defers
if prev == nil { //将_defer从G的延迟链表移除释放_defer
gp._defer = d.link
} else {
prev.link = d.link
}
newd := d.link
freedefer(d)
d = newd
} else {
prev = d
d = d.link
}
}
gp._panic = p.link //上面有对应的赋值,又重新赋了一遍没啥用
for gp._panic != nil && gp._panic.aborted { //循环G中的_panic链表,去掉已经被标记中止的_panic
gp._panic = gp._panic.link
}
if gp._panic == nil { // 如果当前G没有panic, 重置信号为0
gp.sig = 0
}
// 将恢复帧发送给recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 没有更多的延迟调用,现在采用传统的 panic 方式
// 由于在冻结世界之后调用任意用户代码是不安全的,
// 我们调用 preprintpanics 来调用所有必要的 Error
// 和 String 方法,以在 startpanic 之前准备好 panic 字符串。
preprintpanics(gp._panic)
fatalpanic(gp._panic) //触发致命的 panic
*(*int)(nil) = 0 //为了消除编译器的错误提示
}
当我们调度recover时,Go的编译器会将这段代码翻译为CALL runtime.gorecover(SB)
gorecover源码与解读
Go
//代码位置 $GOROOT/src/runtime/panic.go L:1045
func gorecover(argp uintptr) any {
gp := getg() //获取当前G
p := gp._panic // 从当前G中获取一个_panic
// 如果G存在panic,它的状态不为中止,还未进行painc捕获,函数调用参数相同
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
总结
从上面的源码我们可以了解到panic的大致逻辑,当使用panic关键词时,将painc加入到G的_panic链表中去. 调度时 defer func() {recover()}(),会改写_painc中的recovered字段,可恢复的panic必须要recover的配合。 而且这个recover必须位于同一goroutine的直接调用链上,否则无法对 panic 进行恢复,未写完有些细节点还是没读懂,后续查阅资料补充。