美文网首页
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