Go 错误处理

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 程序。

相关推荐
紫洋葱hh1 天前
Go 入门基础:类型、变量与运算符
开发语言·后端·ai·golang
codeejun1 天前
每日一Go·MySQL-7、MySQL 核心原理全景图谱
开发语言·mysql·golang
acd120091 天前
Go 里 err 被不经意覆盖的几个现场
开发语言·后端·golang
倔强的小石头_2 天前
从Go+SQLite编译到浏览器开局:landlord部署、AI对局与固定地址实战
人工智能·golang·sqlite
tachibana22 天前
Go 语言的 Map 是否是并发安全的?
开发语言·数据结构·后端·golang·go·语言
后除2 天前
Go 实战: 使用 Wails v2.15 构建轻量级的桌面应用 - 仿微信登录界面
开发语言·微信·golang
看浪的路人2 天前
第8讲:RAG 生产化——Go 实现亿级向量召回 + 混合检索 + 重排序 Pipeline
开发语言·后端·golang
tachibana22 天前
Go 语言 Map 的扩容过程是怎样的?
开发语言·后端·golang·go·语言
念何架构之路2 天前
go-grpc服务端调用链
开发语言·后端·golang
yuanlaile2 天前
Go 语言全栈实战学习路线:从零基础到云原生微服务的完整拼图
golang·go语言·go实战·go 全栈·微服务与 k8s