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

相关推荐
想你依然心痛15 小时前
AtomCode在后端开发中的实战体验:Go微服务从零搭建
开发语言·微服务·golang
开发小程序的之朴16 小时前
认识安企CMS - 系统概述
nginx·golang·系统架构
雨师@16 小时前
go语言项目--实例化(图书管理)--005
开发语言·后端·golang
Vect__17 小时前
Go 数据结构 slice 深度剖析
开发语言·数据结构·golang
geovindu17 小时前
go: Functional Options Pattern
开发语言·后端·设计模式·golang·函数式选项模式’·惯用法模式
techdashen18 小时前
把正确性藏进类型里:从 Go 的 io.Reader 到 Rust 的 API 设计
网络·golang·rust
必胜刻18 小时前
从零搭建全栈博客系统:Go + Vue 3 + Docker 全流程实战
vue.js·docker·golang
前端之虎陈随易18 小时前
Rust、Golang、MoonBit 编译成 WASM,体积和速度差距有多大?
golang·rust·wasm
雨师@18 小时前
go语言项目--实例化(图书管理)--006
开发语言·后端·golang