首页 > 编程笔记

Go语言异常处理(panic和recover)

本节主要介绍两个内置函数,分别是 panic() 和 recover(),这两个内置函数可以用来处理 Go 程序运行时发生的错误。panic() 函数用于主动抛出错误,recover() 函数用于捕获 panic() 抛出的错误。

宕机(panic)

Go 语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。

一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(线程)中被延迟的函数(defer机制),随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。

引发宕机有如下两种情况:
发生 panic 后,程序会从调用 panic 的函数位置或发生 panic 的地方立即返回,逐层向上执行函数的 defer 语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数而退出。

panic 的参数是一个空接口类型 interface{},所以,任意类型的变量都可以传递给 panic。调用 panic 的方法非常简单,即 panic (xxx)。

panic 不但可以在函数正常流程中抛出,在 defer 逻辑中也可以再次调用 panic 或抛出 panic。defer 中的 panic 能够被后续执行的 defer 捕获。

Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时发现错误,同时减少可能的损失。

Go 语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以,宕机也可以方便地确定发生错误的位置,那么要如何触发宕机呢?例如:
package main
func main() {
    panic("crash")
}
以上代码运行崩溃,如下图所示:


图 1 触发宕机

在以上代码中,只使用了一个内置的函数 panic() 就可以造成崩溃,panic() 函数的声明如下:
func panic(v interface{})  //panic()的参数可以是任意类型
当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用,例如:
package main
import "fmt"
func main() {
    defer fmt.Println("宕机后要做的事情1")
    defer fmt.Println("宕机后要做的事情2")
    panic("宕机")
}
在以上代码中:
运行结果如下图所示:


图 2 宕机时触发延迟执行语句

宕机前,defer 语句会被优先执行,由于第 5 行的 defer 后执行,因此在宕机前,这个 defer 会优先处理,随后才是第 4 行的 defer 对应的语句,这个特性可以用来在宕机发生前进行宕机信息处理。

宕机恢复(recover)

无论代码运行错误是由 Runtime 层抛出的 panic 崩溃,还是主动触发的 panic 崩溃,都可以配合 defer 和 recover 实现错误的捕捉和恢复,让代码发生崩溃后允许继续运行。

recover() 函数用来捕获 panic,阻止 panic 继续向上传递。recover() 函数可以和 defer 语句一起使用,但 recover() 函数只有在 defer 后面的函数体内被直接调用才能捕获 panic 终止异常,否则会返回 nil,异常继续向外传递。

可以有连续多个 panic 被抛出,连续多个被抛出的场景只能出现在延迟调用中。虽然有多个 panic 被抛出,但是只有最后一次的 panic 才能被捕获,例如:
package main
import "fmt"
func main() {
   defer func() {
       if err := recover(); err != nil {
           fmt.Println(err)
       }
   }()
   //只有最后一次的panic调用能够被捕获
   defer func() {
       panic("first defer panic")
   }()
   defer func() {
       panic("second defer panic")
   }()
   panic("main body panic")
}
运行结果为:

first defer panic

包中 init() 函数引发的 panic 只能在 init() 函数中捕获,在 main() 函数中无法被捕获,这是因为 init() 函数优先于 main() 函数执行。

函数并不能捕获内部新启动的 goroutine 所抛出的 panic,例如:
package main
import (
   "fmt"
   "time"
)
func do() {
   //这里并不能捕获da函数中的panic
   defer func() {
       if err := recover(); err != nil {
         fmt.Println(err)
       }
   }()
   go da()
   go db()
   time.Sleep(3 * time.Second)
}
func da() {
   panic("panic da")
   for i := 0; i < 10; i++ {
       fmt.Println(i)
   }
}
func db() {
   for i := 0; i < 10; i++ {
       fmt.Println(i)
   }
}
panic 和 recover 的关系如下:

推荐阅读