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

相关推荐
Zenova EdgeOS2 天前
Go 工业边缘错误处理实战:用 errors 构建可追溯错误链
开发语言·数据库·golang
JavaPub-rodert2 天前
从零搭建通用管理后台:ShiyuAdmin 的 Go、React 与 RBAC 实践
开发语言·react.js·golang
microrain2 天前
SagooIoT 的数据接入层只有四张表,却能接住三种外部数据
物联网·golang·数据建模·sagooiot
fly-iot2 天前
【FDE知识库】(4):企业知识库怎么选型,开源知识库分享几个,选择开源协议友好的版本,开发wiki-go项目
golang·开源·开源协议
福大大架构师每日一题2 天前
excelize v2.11.1发布:Go 1.26.0起步,16项安全修复与嵌入对象能力全面升级
开发语言·安全·golang
小小龙学IT2 天前
Go 语言 pprof 性能剖析与调优深度解析:把“卡在哪“变成一张可执行的数据图
开发语言·后端·golang
十二同学啊2 天前
Go 工程化详解:项目结构、代码规范、测试体系与 CI/CD 实践
ci/cd·golang·代码规范
利威尔xu3 天前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
利威尔xu3 天前
Go IM 源码实战(13):30 个会话只有 2 个红点,未读数错在三条路径
redis·后端·微服务·面试·golang
FungLeo3 天前
成为全栈·Go 后端篇·从 Node SQLite 到 PostgreSQL:把迁移做成独立交付物
postgresql·golang·sqlite·go·数据库迁移·成为全栈