Go 错误处理
Go 语言没有传统意义上的异常(try-catch-finally),而是采用了一种显式的错误处理风格。本文将梳理 Go 中三种"异常"级别:error、panic 和 fatal,以及它们的处理方式。
一、error:可控的正常错误
error 是 Go 中最常见的错误类型,表示一个函数调用中发生了预期内的问题。它不会让程序崩溃,只是把问题返回给调用者,由调用者决定如何处理。
1.1 error 接口
error 是内置接口,定义如下:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都可以作为 error 返回。
1.2 创建 error
最简单的创建方法是 errors.New 或 fmt.Errorf:
err := errors.New("这是一个错误")
err2 := fmt.Errorf("错误码: %d", 404)
在日常开发中,我们经常将常用的错误定义为全局变量:
var ErrNotFound = errors.New("not found")
1.3 自定义 error
如果只需要简单的字符串,errors.New 就够了。但有时需要携带更多信息,可以自定义结构体实现 error 接口:
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("code %d: %s", e.Code, e.Msg)
}
1.4 链式错误(Go 1.13+)
Go 1.13 引入了错误链的概念,允许一个错误包裹另一个错误,形成链条。使用 fmt.Errorf 搭配 %w 动词:
original := errors.New("原始错误")
wrapped := fmt.Errorf("发生错误: %w", original)
包裹后的错误可以通过 Unwrap 解包:
err := wrapped
for err != nil {
fmt.Println(err)
err = errors.Unwrap(err)
}
1.5 错误判断:Is 与 As
因为错误可能被包裹,直接用 == 比较往往无效。标准库提供了两个函数:
-
errors.Is(err, target):判断错误链中是否存在 特定错误值(通常指向预先定义的全局错误)。 -
errors.As(err, target):将错误链中 第一个匹配类型的错误 赋值给目标指针,用于获取更详细的错误信息。
示例:
if errors.Is(err, ErrNotFound) {
// 处理未找到的情况
}
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("错误码:", myErr.Code)
}
1.6 缺点与社区方案
Go 原生错误处理的主要缺点:
-
没有堆栈信息(第三方包
github.com/pkg/errors可以弥补) -
if err != nil大量重复 -
自定义错误是变量而非常量,容易被修改
尽管如此,这种显式处理方式也带来了清晰的控制流和易于调试的优点。
二、panic:严重但可恢复的运行时异常
panic 表示程序无法继续执行的严重问题,如数组越界、向 nil map 写入等。panic 发生时,当前函数立即停止,执行当前函数的 defer,然后逐级返回,直到程序崩溃。
2.1 主动触发 panic
使用内置函数 panic:
func initDB(addr string) {
if addr == "" {
panic("数据库地址不能为空")
}
}
2.2 善后:defer 的执行
即使发生 panic,已经注册的 defer 仍然会执行,这给了我们清理资源的机会。
func main() {
defer fmt.Println("cleanup")
panic("oops")
}
// 输出 cleanup 之后才打印 panic 信息
2.3 恢复:recover
recover 是内置函数,必须在 defer 函数中调用。它可以捕获 panic,使程序恢复正常执行。
defer func() {
if r := recover(); r != nil {
fmt.Println("恢复了:", r)
}
}()
panic("出错了")
// 程序不会崩溃,会继续往下执行
使用 recover 的注意事项:
-
只能在
defer中直接调用(如果嵌套在匿名函数中再调用recover可能无效)。 -
多次
defer中也只有一个能捕获。 -
传给
panic的参数不能是nil(否则虽然会恢复,但看不到任何信息)。
2.4 多协程下的 panic
注意:一个 goroutine 中的 panic 如果不被 recover,会导致整个程序崩溃,其他 goroutine 的 defer 也无法保证执行。因此,在启动 goroutine 时,通常要在其入口处放置 recover 保护。
三、fatal:不可恢复的致命错误
fatal 并非 Go 的内置概念,而是指那些导致程序必须立即终止的错误,通常通过 os.Exit 实现。与 panic 不同,os.Exit 不会执行任何 defer,也不会打印堆栈信息。
if err != nil {
fmt.Println("严重错误,退出")
os.Exit(1)
}
一般只有在无法继续运行的情况下才使用 fatal,例如配置文件缺失、端口被占用等。
四、总结
| 级别 | 是否可恢复 | 典型场景 | 处理方式 |
|---|---|---|---|
| error | 是 | 文件不存在、网络超时 | 返回 error 值,调用者处理 |
| panic | 是(通过 recover) | 数组越界、向 nil map 写入 | recover 捕获,或者让它崩溃 |
| fatal | 否 | 配置错误、关键资源缺失 | os.Exit,无法恢复 |
Go 的错误处理哲学是"把错误当作普通值"。理解好 error、panic 和 fatal 的区别,能够帮助你写出更可靠的 Go 程序。