美文网首页
go 的 revover 实现原理

go 的 revover 实现原理

作者: wayyyy | 来源:发表于2022-08-31 01:59 被阅读0次
image.png

recover() 函数实际被调用的是src/runtime/panic.go:gorecover()

gorecover

runtime.gorecover 函数实现很简短

func gorecover(arg uintptr) interface{} {
    gp := getg()
    p := gp._panic()  // 获取panic实例,只有发生了panic,实例才不为nil
    if p != nil && !p.goexit && !p.recovered && arg == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }    

    return nil
}
  • 恢复逻辑
    runtime.gorecover()函数通过协程数据结构中的_panic得到当前 panic 实例(上面代码中的 p),如果当前panic的状态支持recover,则给该 panic实例标记 recovered状态(p.recovered= true),最后返回panic()函数的参数(p.arg)。

    另外,当前执行 recover() 函数的 defer 函数是被 runtime.gopanic()执行的,defer 函数执行 结束以后,在runtime.gopanic()函数中会检查panic实例的recovered状态,如果发现 panic被恢 复,则runtime.gopanic()将结束当前panic流程,将程序流程恢复正常。

  • 生效条件
    通过代码p != nil && !p.goexit && !p.recovered && arg == uintptr(p.argp)可以看到需要满足四个条件才可以恢复panic,且四个条件缺一不可:

    • p!=nil:必须存在 panic
    • !p.goexit:非 runtime.goexit
    • !p.recovered:panic 还未被恢复;
    • argp == uintptr(p.argp):recover 必须被 defer() 直接调用。

    当前协程没有产生panic时,协程结构体中panic的链表为空,不满足恢复条件。

    当程序运行runtime.goexit时也会创建一个 panic 实例,会标记该实例的 goexit 属性为 true,但该类型的 panic 不能被恢复。

    假设函数包含多个defer函数,前面的 defer通过 recover() 函数消除panic后,函数中剩余的 defer 仍然会执行,但不能再次recover(),如以下代码所示,函数第一行 defer 中的 recover() 将返回 nil。

    func foo() {
        defer func()  {  recover()  }()  // 恢复无效,因为_panic.recovered ==true 
        defer func()  {  recover()  }()  // 标记_panic.recovered=true
        panic("err")
    }
    

    内置函数 recover()没有参数,runtime.gorecover()函数却有参数,为什么呢?
    这正是为了限制recover()函数必须被defer直接调用。runtime.gorecover() 函数的参数为调用 recover() 函数的参数地址,通常是 defer 函数的参数地址,panic 实例中也保存了当前 defer函数的参数地址,如果二者一致,说明 recover被defer函数直接调用。举例如下:

    func foo() {
        defer func() {  // 假设函数为A
            func()  {  // 假设函数为B
                // runtime.gorecover,传入函数B的参数地址   
                // argp==uintptr(p.argp),检测失败,无法恢复 
                if err :=recover(); err!=nil {
                    fmt.Print1n("A")
                }
            }
        }
    }
    
总结

通过以上分析,我们可以很好地回答以下问题了:

  1. 为什么recover()函数一定要在defer()函数中才生效?
    如果recover()函数不在defer()函数中,那么defer()函数可能出现在panic之前,也可能出现在panic之后。出现在panic之前,因为找不到panic实例而无法生效,出现在panic之后,代码没有机会执行,所以recover()函数必须存在于recover()函数中才会生效。

  2. panic 被 recover 之后,无法再次被 recover 捕获

  3. 假如defer()函数中调用了函数A,为什么A中的recover()不能生效?

相关文章

网友评论

      本文标题:go 的 revover 实现原理

      本文链接:https://www.haomeiwen.com/subject/digrnrtx.html