前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >go panic探索

go panic探索

作者头像
Michel_Rolle
修改2023-11-12 10:07:24
6260
修改2023-11-12 10:07:24
举报
文章被收录于专栏:golang分享golang分享

panic 发生之后,如果 Go 不做任何特殊处理,默认行为是打印堆栈,退出程序。

panic 到底是什么?

  1. panic( ) 函数内部会产生一个关键的数据结构体 _panic ,并且挂接到 goroutine 之上;
  2. panic( ) 函数内部会执行 _defer 函数链条,并针对 _panic 的状态进行对应的处理;

什么叫做 panic( ) 的对应的处理?

循环执行 goroutine 上面的 _defer 函数链,如果执行完了都还没有恢复 _panic 的状态,那就没得办法了,退出进程,打印堆栈。

如果在 goroutine 的 _defer 链上,有个朋友 recover 了一下,把这个 _panic 标记成恢复,那事情就到此为止,就从这个 _defer 函数执行后续正常代码即可,走 deferreturn 的逻辑。

recover 函数

recover 对应了 runtime/panic.go 中的 gorecover 函数实现。

代码语言:javascript
复制
func gorecover(argp uintptr) interface{} {
    // 只处理 gp._panic 链表最新的这个 _panic;
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

这个函数可太简单了:

  1. 取出当前 goroutine 结构体;
  2. 取出当前 goroutine 的 _panic 链表最新的一个 _panic,如果是非 nil 值,则进行处理;
  3. 该 _panic 结构体的 recovered 赋值 true,程序返回;

这就是 recover 函数的全部内容,只给 _panic.recovered 赋值而已,不涉及代码的神奇跳转。而 _panic.recovered 的赋值是在 panic 函数逻辑中发挥作用。

panic函数

panic 的实现在一个叫做 gopanic 的函数,位于 runtime/panic.go 文件。panic 机制最重要最重要的就是 gopanic 函数了,所有的 panic 细节尽在此。为什么 panic 会显得晦涩,主要有两个点:

  1. 嵌套 panic 的时候,gopanic 会有递归执行的场景;
  2. 程序指令跳转并不是常规的函数压栈,弹栈,在 recovery 的时候,是直接修改指令寄存器的结构体,从而直接越过了 gopanic 后面的逻辑,甚至是多层 gopanic 递归的逻辑;

一切秘密都在下面这个函数:

代码语言:javascript
复制
// runtime/panic.go
func gopanic(e interface{}) {
    // 在栈上分配一个 _panic 结构体
    var p _panic
    // 把当前最新的 _panic 挂到链表最前面
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    
    for {
        // 取出当前最近的 defer 函数;
        d := gp._defer
        if d == nil {
            // 如果没有 defer ,那就没有 recover 的时机,只能跳到循环外,退出进程了;
            break
        }

        // 进到这个逻辑,那说明了之前是有 panic 了,现在又有 panic 发生,这里一定处于递归之中;
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
            }
            // 把这个 defer 从链表中摘掉;
            gp._defer = d.link
            freedefer(d)
            continue
        }

        // 标记 _defer 为 started = true (panic 递归的时候有用)
        d.started = true
        // 记录当前 _defer 对应的 panic
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

        // 执行 defer 函数
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

        // defer 执行完成,把这个 defer 从链表里摘掉;
        gp._defer = d.link
        
        // 取出 pc,sp 寄存器的值;
        pc := d.pc
        sp := unsafe.Pointer(d.sp)
        // 如果 _panic 被设置成恢复,那么到此为止;
        if p.recovered {
            // 摘掉当前的 _panic
            gp._panic = p.link
            // 如果前面还有 panic,并且是标记了 aborted 的,那么也摘掉;
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            // panic 的流程到此为止,恢复到业务函数堆栈上执行代码;
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            // 注意:恢复的时候 panic 函数将从此处跳出,本 gopanic 调用结束,后面的代码永远都不会执行。
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    // 打印错误信息和堆栈,并且退出进程;
    preprintpanics(gp._panic)
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

上面逻辑可以拆分为循环内和循环外两部分去理解:

  • 循环内:程序执行 defer,是否恢复正常的指令执行,一切都在循环内决定;
  • 循环外:一旦走到循环外,说明 _panic 没人处理,程序即将退出;

for 循环内

循环内的事情拆解成:

  1. 遍历 goroutine 的 defer 链表,获取到一个 _defer 延迟函数;
  2. 获取到 _defer 延迟函数,设置标识 d.started,绑定当前 d._panic(用以在递归的时候判断);
  3. 执行 _defer 延迟函数;
  4. 摘掉执行完的 _defer 函数;
  5. 判断 _panic.recovered 是否设置为 true,进行相应操作;
    1. 如果是 true 那么重置 pc,sp 寄存器(一般从 deferreturn 指令前开始执行),goroutine 投递到调度队列,等待执行;
  6. 重复以上步骤;

问题一:为什么 recover 一定要放在 defer 里面才生效?

因为,这是唯一的修改 _panic.recovered 字段的时机 !

为什么 recover 已经放在 defer 里面,但是进程还是没有恢复?

划重点:在 gopanic 里,只遍历执行当前 goroutine 上的 _defer 函数链条。所以,如果挂在其他 goroutine 的 defer 函数做了 recover ,那么没有丝毫用途。

代码语言:javascript
复制
func main() { // g1
    go func() { // g2
        defer func() {
            recover()
        }()
    }()
    panic("test")
}

因为,panic 和 recover 在两个不同的 goroutine,_panic 是挂在 g1 上的,recover 是在 g2 的 _defer 链条里。

gopanic 遍历的是 g1 的 _defer 函数链表,跟 g2 八杆子打不着,g2 的 recover 自然拿不到 g1 的 _panic 结构,自然也不能设置 recovered 为 true ,所以程序还是崩了。

recover 函数

在 gopanic 函数中,在循环执行 defer 函数的时候,如果发现 _panic.recovered 字段被设置成 true 的时候,调用 mcall(recovery) 来执行所谓的恢复。

看一眼 recovery 函数的实现,这个函数极其简单,就是恢复 pc,sp 寄存器,重新把 Goroutine 投递到调度队列中。

代码语言:javascript
复制
// runtime/panic.go
func recovery(gp *g) {
    // 取出栈寄存器和程序计数器的值
    sp := gp.sigcode0
    pc := gp.sigcode1
    // 重置 goroutine 的 pc,sp 寄存器;
    gp.sched.sp = sp
    gp.sched.pc = pc
    // 重新投入调度队列
    gogo(&gp.sched)
}

总结

  1. panic() 会退出进程,是因为调用了 exit 的系统调用;
  2. recover() 所在的 defer 函数必须和 panic 都是挂在同一个goroutine 上,不能跨协程,因为 gopanic 只会执行当前 goroutine 的延迟函数;

参考

深度细节 | Go 的 panic 的秘密都在这

源码剖析panic与recover

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档