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

相关推荐
会编程的土豆5 小时前
结构体 struct:用代码描述用户、订单、座位
数据库·mysql·golang
llwszx21 小时前
【Java/Go后端手撸原生Agent(第九篇):Plan-and-Execute规划模式——从“走一步看一步“到“先谋后动“】
java·python·golang·agent开发·plan模式·规划执行模式
灯澜忆梦1 天前
GO_网络编程---文件传输实战
网络·golang·php
北冥you鱼1 天前
Go 语言新手扫盲:指针 * 和 & 使用场景详解
开发语言·后端·golang
Wang's Blog1 天前
Go-Zero项目开发17: IM私聊功能实现与消息存储设计
开发语言·后端·golang
名字还没想好☜1 天前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
编程序的员1 天前
使用OpenTelemetry来监控GoLang应用
运维·golang·monitoring·apm·opentelemetry
ttwuai2 天前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
布朗克1682 天前
Go 入门到精通-31-编码与序列化
开发语言·qt·golang·编码与序列化
microrain2 天前
从硬编码到热插拔:SagooIoT插件架构如何打破物联网平台的扩展瓶颈
物联网·golang·开源·sagooiot