Go语言异常处理(panic和recover)
本节主要介绍两个内置函数,分别是 panic() 和 recover(),这两个内置函数可以用来处理 Go 程序运行时发生的错误。panic() 函数用于主动抛出错误,recover() 函数用于捕获 panic() 抛出的错误。
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 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 信息输出到控制台,所以,宕机也可以方便地确定发生错误的位置,那么要如何触发宕机呢?例如:
图 1 触发宕机
在以上代码中,只使用了一个内置的函数 panic() 就可以造成崩溃,panic() 函数的声明如下:
运行结果如下图所示:
图 2 宕机时触发延迟执行语句
宕机前,defer 语句会被优先执行,由于第 5 行的 defer 后执行,因此在宕机前,这个 defer 会优先处理,随后才是第 4 行的 defer 对应的语句,这个特性可以用来在宕机发生前进行宕机信息处理。
recover() 函数用来捕获 panic,阻止 panic 继续向上传递。recover() 函数可以和 defer 语句一起使用,但 recover() 函数只有在 defer 后面的函数体内被直接调用才能捕获 panic 终止异常,否则会返回 nil,异常继续向外传递。
可以有连续多个 panic 被抛出,连续多个被抛出的场景只能出现在延迟调用中。虽然有多个 panic 被抛出,但是只有最后一次的 panic 才能被捕获,例如:
函数并不能捕获内部新启动的 goroutine 所抛出的 panic,例如:
宕机(panic)
Go 语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(线程)中被延迟的函数(defer机制),随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。
引发宕机有如下两种情况:
- 程序主动调用 panic() 函数;
- 程序产生运行时错误,由运行时检测并抛出。
发生 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("宕机") }在以上代码中:
- 第 4 行和第 5 行使用 defer 语句延迟了两个语句;
- 第 6 行发生宕机。
运行结果如下图所示:
图 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 的关系如下:
- 有 panic 没 recover,程序宕机;
- 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 语句后,从宕机点退出当前函数后继续执行。