golang recover

使用

在 Golang 中可以通过 recover 来捕获 panic 恢复程序,使用时需要注意:

  1. recover 只能捕获 当前协程 的 panic,类似下面的方式捕获会失败:

    package main
    
    import (
      "log"
    )
    
    func main() {
      // Recover outside a goroutine
      defer func() {
        log.Println(recover())
      }()
    
      go func() {
        // Panic occurs in a goroutine
        panic("A bad boy stole a server")
      }()
    }
    
  2. recover 只在 deferred 函数中有效,间接 defer 调用也不会成功:

    package main
    
    import (
      "fmt"
      "log"
      "time"
    )
    
    func getItem() {
      go func() {
        // Call Recover() Tool Function.
        defer func() {
          Recover("Panic in goroutine")
        }()
        // Panic here.
        panic("A bad boy stole the server")
        // Test the panic result.
        fmt.Println("Will NOT Reach Here")
      }()
    }
    
    func Recover(funcName string) {
      if err := recover(); err != nil {
        // If the panic is catched.
        log.Printf("panic para: %v, panic info: %v\n", funcName, err)
      }
    }
    
    func main() {
      getItem() // Fail to catch the panic, program will crash
      time.Sleep(time.Second)
    }
    

官方文档中的描述:

The return value of recover is nil if any of the following conditions holds:

  • panic's argument was nil;
  • the goroutine is not panicking;
  • recover was not called directly by a deferred function.

当 pnaic 参数为 nil 时,虽然 recover 返回值也是 nil,但当前 panic 仍然会被被标记为 recovered,并恢复程序的执行。

原理

在 Golang 中程序运行的基本单位为 Goroutine,在 runtime 中每个 Goroutine 都对应一个结构体 g,在结构体 g 上存在有 panic 和 defer 链表:

type g struct {
  _panic *_panic
  _defer *_deder
}

当代码中出现 panic 会,会创建一个 _panic 结构绑定到当前 Goroutine 上并遍历当前 Goroutine 上绑定的 _defer 链表执行:

func gopanic(e interface{}) {
  gp := getg()

  var p _panic
  p.arg = e
  p.link = gp._panic
  gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

  for {
    d := gp._defer
    if d == nil {
      break
    }

    d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    p.argp = unsafe.Pointer(getargp(0))  // 指向当前 defer 函数的参数列表指针
    reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
    p.argp = nil

    d._panic = nil
    d.fn = nil
    gp._defer = d.link

    freedefer(d)
    if p.recovered {
      //...
    }
  }

  fatalpanic(gp._panic)
  *(*int)(nil) = 0
}

而在执行 defer 中的 recover 时,会判断当前 _panic 上的 argp 和当前 argp 是否相同:

func gorecover(argp uintptr) interface{} {
  // Must be in a function running as part of a deferred call during the panic.
  // Must be called from the topmost function of the call
  // (the function used in the defer statement).
  // p.argp is the argument pointer of that topmost deferred function call.
  // Compare against argp reported by caller.
  // If they match, the caller is the one who can recover.
  gp := getg()
  p := gp._panic
  if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
    p.recovered = true
    return p.arg
  }
  return nil
}

因此,如果 recover 不在当前 Goroutine 的 deferred 函数中,那么就不会执行 p.recovered = true 语句, 而 runtime.gopanic 函数依赖于 _panic.recovered 的值来判断是否恢复代码的执行。

posted @ 2021-11-27 17:15  rgb-24bit  阅读(24)  评论(0编辑  收藏  举报
隐藏