[Go] panic 和 recover

通常情况下,函数向其调用方报告错误的方式都是返回一个 error 类型的值。但是,当遇到致命错误的时候,很可能会使程序无法继续运行。这时,上述错误处理方式就太不适合了,Go 推荐通过调用 panic 函数来报告致命错误。

1. panic

为了报告运行期间的致命错误,Go 内建了专用函数 panic,该函数用于停止当前的控制流程并引发一个运行时恐慌。它可以接受一个任意类型的参数值,不过这个参数值的类型常常会是 string 或者 error,因为这样更容易描述运行时恐慌的详细信息。请看下面的例子:

func main()  {
        outerFunc()
}

func outerFunc()  {
        innerFunc()
}

func innerFunc()  {
        panic(errors.New("An intended fatal error!"))
}

当调用 innerFunc 函数中的 panic 函数后,innerFunc 的执行会被停止。紧接着,流程控制权会交回调用方 outerFunc 函数。然后,outerFunc 函数的执行也将被停止。运行时恐慌就这样沿着调用栈反方向进行传播,直至到达当前 goroutine 的调用栈的最顶层。一旦达到顶层,就意味着该 goroutine 调用栈中所有函数的执行都已经被停止了,程序已经崩溃。

当然,运行时恐慌并不都是通过调用 panic 函数的方式引发的,也可以由 Go 的运行时系统来引发。例如:

myIndex := 4
ia := [3]int{1, 2, 3}
_ = ia[myIndex]

这个示例中的第 3 行代码会引发一个运行时恐慌,因为它造成了一个数组访问越界的运行时错误。这个运行时恐慌就是由 Go 的运行时系统报告的。它相当于我们显示地调用 panic 函数并传入一个 runtime.Error 类型的参数值。顺便说一句,runtime.Error 是一个接口类型,并且内嵌了 Go 内置的 error 接口类型。

显然,我们都不希望程序崩溃。那么,怎样“拦截”一个运行时恐慌呢?

2. recover

运行时恐慌一旦被引发,就会向调用方传播直至程序崩溃。Go 提供了专用于“拦截”运行时恐慌的内建函数 recover,它可以使当前的程序从恐慌状态中恢复并重新获得流程控制权。recover 函数被调用后,会返回一个 interface{} 类型的结果。如果当时的程序正处于运行时恐慌的状态,那么这个结果就会是 非 nil 的。

recover 函数应该与 defer 语句配合起来使用,例如:

defer func() {
    if p := recover(); p != nil {
        fmt.Printf("Recovered panic: %s\n", p)
    }
}()

把此类代码放在函数体的开始处,这样可以有效防止该函数及其下层调用中的代码引发运行时恐慌。一旦发现 recover 函数的调用结果是 非 nil,就应该采取相应的措施。

值得一提的是,Go 标准库中有一种常见的用法值得我们参考。请看标准库代码包 fmt 中的 Token 函数的部分声明:

func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
        defer func() {
                if e := recover(); e != nil {
                        if se, ok := e.(scanError); ok {
                                err = se.err
                        } else {
                                panic(e)
                        }
                }
        }()
        // 省略部分代码
}

在 Token 函数包含的 延迟函数 中,当运行时恐慌携带值的类型是 fmt.scanError 时,这个值就会被赋值给代表结果值的变量 err,否则运行时恐慌就会被重新引发。如果这个重新引发的运行时恐慌传递到了调用栈的最顶层,那么标准输出上就会打印类似这样的内容:

panic: <运行时恐慌被首次引发时携带的值的字符串形式> [recovered]
    panic: <运行时恐慌被重新引发时携带的值的字符串形式>
goroutine 1 [running]:
main.func.001()
<调用栈信息>

goroutine 2 [runnable:]
exit status 2    

这里展现的惯用法有 2 个,如下:

  1. 可以把运行时恐慌的携带值转换为 error 类型值,并当作常规结果返回给调用方。这样既阻止了恐慌的扩散,又传递了引起恐慌的原因。
  2. 检查运行时恐慌携带值的类型,并根据类型做不同的后续动作,这样可以精确地控制程序的错误处理行为。

摘自:《Go 并发编程实战(第二版) . 郝林》