09-异常处理 panic 和 recover

异常处理相关的两个内置函数 panic()recover(),这两个内置函数可以用来处理 Go 程序运行时发生的错误。panic() 函数用于主动抛出错误,recover() 函数用于捕获 panic() 抛出的错误。

宕机(panic)

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

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

引发宕机有如下两种情况:

  • 程序主动调用 panic() 函数,这样开发者可以及时发现错误,同时减少可能的损失。Go 语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以,宕机也可以方便地确定发生错误的位置
  • 程序产生运行时错误,由运行时检测并抛出。

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

panic 的参数是一个空接口类型 interface{},所以,任意类型的变量都可以传递给 panic。panic() 函数的声明如下:

1
func panic(v interface{})  //panic()的参数可以是任意类型

调用 panic 的方法非常简单,即 panic (xxx)。例如:

1
2
3
4
package main
func main() {
panic("crash")
}

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

panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用,例如:

1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
defer fmt.Println("宕机后要做的事情1") // 运行
defer fmt.Println("宕机后要做的事情2") // 运行
panic("宕机")
defer fmt.Println("宕机后要做的事情3") // 不运行
}

运行结果如下所示:

1
2
3
4
5
6
7
宕机后要做的事情2
宕机后要做的事情1
panic: 宕机

goroutine 1 [running]:
main.main()
/Users/fenglepeng1/code/test/panic_t.go:14 +0x90

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

宕机恢复(recover)

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

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

可以有连续多个 panic 被抛出,连续多个被抛出的场景只能出现在延迟调用中。虽然有多个 panic 被抛出,但是只有最后一次的 panic 才能被捕获,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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")
}

运行结果为:

1
first defer panic

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

函数并不能捕获内部新启动的 goroutine 所抛出的 panic,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 语句后,从宕机点退出当前函数后继续执行。

Reference


09-异常处理 panic 和 recover
https://flepeng.github.io/021-Go-31-Go-基础-09-异常处理-panic-和-recover/
作者
Lepeng
发布于
2024年12月2日
许可协议