1.15 异常机制:panic 和 recover
Go 语言在错误处理上推崇“显式错误处理”,与许多其他语言中的 try-catch 机制不同,Go 语言将错误视为函数返回值的一部分,使用 panic 和 recover 机制来处理那些无法恢复的、导致程序崩溃的“异常”情况。
一、error 接口
在 Go 语言中,最常用的错误处理方式是返回一个实现了内置 error 接口的值。
1. error 接口定义
error 是一个内置接口,定义非常简单:
go
type error interface {
Error() string
}任何实现了 Error() string 方法的类型都可以作为错误类型。
2. 创建和返回错误
标准库中提供了两种创建 error 对象的方法:
| 函数 | 用法 | 描述 |
|---|---|---|
errors.New(text string) | errors.New("文件未找到") | 创建一个包含给定错误信息的简单错误。 |
fmt.Errorf(format string, a ...interface{}) | fmt.Errorf("无效参数: %v", arg) | 格式化输出错误信息,通常用于包含变量或上下文信息。 |
示例代码:
go
package main
import (
"errors"
"fmt"
)
// 定义一个表示除零错误的变量
var ErrDivByZero = errors.New("division by zero")
// div 函数返回结果和错误
func div(x, y int) (int, error) {
if y == 0 {
// 返回 0 和自定义错误
return 0, ErrDivByZero
}
// 返回计算结果和 nil(表示没有错误)
return x / y, nil
}
func main() {
// 成功调用
result1, err1 := div(10, 2)
if err1 != nil {
fmt.Println("错误:", err1)
} else {
fmt.Println("10 / 2 =", result1) // 输出: 10 / 2 = 5
}
// 失败调用(除零)
result2, err2 := div(10, 0)
if err2 != nil {
// 可以通过比较错误变量来判断具体错误类型
if errors.Is(err2, ErrDivByZero) {
fmt.Println("捕获到除零错误:", err2)
} else {
fmt.Println("其他错误:", err2)
}
} else {
fmt.Println("10 / 0 =", result2)
}
}3. 错误判断
处理错误时,通常使用以下方法:
if err != nil: 这是最常见的方式,用来检查函数调用是否返回了错误。errors.Is(err, target): 用于判断错误链中是否包含特定的错误值(例如上面定义的ErrDivByZero),这是进行错误类型判断的标准方式。errors.As(err, &target): 用于判断错误链中是否可以解包(Unwrap)出特定类型的错误,并将其赋值给target变量。
二、panic 和 recover
panic 和 recover 是 Go 语言处理不可恢复错误的机制,类似于其他语言中的异常抛出和捕获。
惯例: 只有当程序遇到无法继续运行的严重错误(如数组越界、空指针引用或关键服务失败)时,才应该使用 panic。对于预期的、可恢复的错误,应使用 error 接口。
1. panic:抛出异常,终止程序
panic 是一个内置函数,用于中断正常的控制流程。
- 当函数
F中执行panic时,F之后的代码将不再执行。 - 程序会沿着调用栈向外层函数返回,执行当前 Goroutine 中所有已注册的
defer函数。 - 如果调用栈上的所有
defer函数都执行完毕,但panic仍未被捕获,程序将打印堆栈信息并崩溃退出。
示例:
go
func main() {
defer fmt.Println("这是 main 函数的 defer") // 会在 panic 发生后执行
fmt.Println("开始执行")
// 手动触发 panic
panic("发生了一个严重错误!")
fmt.Println("这行代码不会被执行")
}
// 输出:
// 开始执行
// 这是 main 函数的 defer
// panic: 发生了一个严重错误!
// ... (堆栈信息)2. defer:延迟调用
defer 语句用于延迟一个函数的执行,直到包含它的函数(或方法)即将返回时才执行。在异常处理中,defer 是与 recover 搭配使用的关键。
defer语句会在其所在函数返回前执行,无论函数是正常返回还是因为panic而退出。- 多个
defer语句的执行顺序是栈式的,即后进先出 (LIFO)。
3. recover:捕获异常,恢复程序
recover 是一个内置函数,必须在 defer 函数中调用,才能捕获到 panic 抛出的错误,并阻止程序崩溃。
- 有效调用: 在
defer函数内直接调用recover()。 - 作用: 如果一个
panic正在进行,recover会捕获到panic的参数(即panic()函数的参数),并终止panic过程,让程序恢复正常执行流程。 - 恢复位置: 捕获
panic后,程序不会回到panic发生的点,而是继续执行defer之后的代码。
示例代码:
go
package main
import "fmt"
func safeDivide(a, b int) {
// 必须在 defer 匿名函数中调用 recover
defer func() {
if r := recover(); r != nil {
// r 就是 panic() 传递的参数
fmt.Printf("程序恢复!捕获到 panic 错误: %v\n", r)
}
}()
fmt.Println("尝试进行除法计算...")
// 故意制造一个会导致 panic 的操作
z := a / b // 如果 b=0,会触发 runtime panic: integer divide by zero
fmt.Printf("%d / %d = %d\n", a, b, z)
}
func main() {
safeDivide(10, 2) // 正常执行
fmt.Println("--------------------")
safeDivide(10, 0) // 发生 panic,但被 recover 捕获并恢复
// 由于 panic 被捕获,程序可以继续执行后续代码
fmt.Println("--------------------")
fmt.Println("main 函数继续执行,程序没有崩溃。")
}输出结果:
尝试进行除法计算...
10 / 2 = 5
--------------------
尝试进行除法计算...
程序恢复!捕获到 panic 错误: runtime error: integer divide by zero
--------------------
main 函数继续执行,程序没有崩溃。三、 panic 无法跨越 Goroutine 边界
panic 和 recover 机制是针对单个 Goroutine 的。在一个 Goroutine 中触发的 panic,只能被该 Goroutine 调用栈上的 defer 函数捕获。
核心结论:
- 主协程的
recover无法捕获子协程的panic。 - 子协程发生
panic且未被捕获时,它会导致整个程序崩溃,而不仅仅是该子协程。
子协程中的 panic
go
package main
import (
"fmt"
"time"
)
func main() {
// ❌ 错误做法:这个 recover 无法捕获子协程的 panic
defer func() {
if r := recover(); r != nil {
fmt.Println("❌ 主协程捕获了错误:", r)
}
}()
fmt.Println("主协程开始运行...")
go func() {
// 子协程中没有 recover
fmt.Println("子协程即将 panic")
panic("来自子协程的致命错误") // ⚠️ 这个 panic 会使整个程序崩溃!
}()
// 确保子协程有时间运行
time.Sleep(1 * time.Second)
fmt.Println("主协程结束。") // 如果程序崩溃,这句不会被输出
}在子协程内部捕获
为了保护程序不被子协程的 panic 影响,正确做法:必须在子协程的入口函数中设置 defer + recover。
go
package main
import (
"fmt"
"time"
)
func worker() {
// ✅ 正确做法:在子协程内部设置 recover
defer func() {
if r := recover(); r != nil {
fmt.Printf("✅ 子协程恢复!错误: %v\n", r)
}
}()
fmt.Println("子协程开始工作...")
// ... 可能触发 panic 的代码 ...
panic("来自子协程的致命错误")
// ... panic 后续代码不会执行 ...
}
func main() {
go worker()
// 即使 worker 发生了 panic,程序也不会崩溃
time.Sleep(1 * time.Second)
fmt.Println("主协程继续运行,程序安全。")
}四、总结与建议
| 机制 | 作用 | 适用场景 | Go 语言惯例 |
|---|---|---|---|
error | 显式返回,表示函数执行结果中的预期错误。 | 文件未找到、网络连接失败、输入参数无效等可预测和可恢复的错误。 | 推荐:大部分情况下使用 error 接口进行错误处理。 |
panic/recover | 抛出/捕获异常,处理程序中不可恢复的运行时错误。 | 数组越界、空指针引用、关键服务初始化失败等不可恢复的错误。 | 仅用于处理真正的程序异常/崩溃,或在底层库中将 panic 转换为 error。 |