Go 错误处理

Go 错误处理

Go 语言没有传统意义上的异常(try-catch-finally),而是采用了一种显式的错误处理风格。本文将梳理 Go 中三种"异常"级别:errorpanicfatal,以及它们的处理方式。

一、error:可控的正常错误

error 是 Go 中最常见的错误类型,表示一个函数调用中发生了预期内的问题。它不会让程序崩溃,只是把问题返回给调用者,由调用者决定如何处理。

1.1 error 接口

error 是内置接口,定义如下:

复制代码
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为 error 返回。

1.2 创建 error

最简单的创建方法是 errors.Newfmt.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 错误判断:IsAs

因为错误可能被包裹,直接用 == 比较往往无效。标准库提供了两个函数:

  • 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 的错误处理哲学是"把错误当作普通值"。理解好 errorpanicfatal 的区别,能够帮助你写出更可靠的 Go 程序。

相关推荐
geovindu3 小时前
go: Generators Pattern
开发语言·后端·设计模式·golang·生成器模式
青春喂了后端8 小时前
Go Sidecar Status 性能优化
开发语言·性能优化·golang
A__tao8 小时前
告别手写 Go 结构体!推荐一个支持注释解析的 YAML 转 Struct 在线工具
开发语言·后端·golang
何以解忧,唯有..8 小时前
Go 语言语句分隔符详解:分号、换行与代码规范
开发语言·golang·代码规范
踏着七彩祥云的小丑10 小时前
Go学习第8天:接口 + 泛型 + 错误处理
开发语言·学习·golang·go
2501_9318037511 小时前
Go 泛型核心解析:从类型参数到约束设计
golang
java_cj11 小时前
从kubectl源码学Cobra:打造专业级Go命令行工具的完整实践
运维·开发语言·后端·云原生·golang·kubernetes·k8s
jieyucx11 小时前
Go MongoDB 实战完全指南|从连接、CRUD、BSON结构体映射到高并发避坑全解
开发语言·mongodb·golang
humcomm12 小时前
Go语言在AI领域的最新进展(2026年上半年)
开发语言·人工智能·golang