Skip to content

Go 错误处理

Go 不使用异常(exception),而是通过返回 error 值来显式处理错误。这是 Go 错误处理哲学的核心:错误是值,应被检查和处理。

error 接口

error 是一个内建接口:

go
type error interface {
    Error() string
}

基本错误处理模式

go
f, err := os.Open("file.txt")
if err != nil {
    return err          // 向上传播
}
defer f.Close()

// 配合 defer 确保资源释放

错误包装(Go 1.13+)

使用 %w 包装底层错误,保留原始错误链以便追溯:

go
if err != nil {
    return fmt.Errorf("读取配置失败: %w", err)
}

// 不保留链(仅格式化字符串)用 %v
return fmt.Errorf("读取配置失败: %v", err)

errors.Is / errors.As

go
// errors.Is:判断错误链中是否包含某个目标错误(哨兵错误)
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("文件不存在")
}

// errors.As:将错误链中的特定类型提取出来
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("路径错误:", pathErr.Path)
}

自定义错误

实现 error 接口即可定义自己的错误类型:

go
type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}

func doSomething() error {
    return &MyError{Code: 400, Message: "参数错误"}
}

// 调用方用 errors.As 提取
if err := doSomething(); err != nil {
    var myErr *MyError
    if errors.As(err, &myErr) {
        fmt.Println("错误码:", myErr.Code)
    }
}

哨兵错误(Sentinel Errors)

预定义的包级错误变量,用于调用方做 errors.Is 比较:

go
var (
    ErrNotFound  = errors.New("not found")
    ErrForbidden = errors.New("forbidden")
)

约定:哨兵错误变量名以 Err 开头。

panic 与 recover

panic 用于真正不可恢复的程序错误(类似其他语言的异常),recover 用于捕获 panic 防止程序崩溃(通常仅用于库/服务器顶层兜底)。

go
// 触发 panic
panic("something went terribly wrong")

// recover 必须在 defer 中调用才有效
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

实践原则:

  • 不要用 panic 做常规错误控制,常规错误一律返回 error
  • panic 仅用于:程序无法继续运行的 invariants 被破坏(如初始化失败)。
  • recover 仅用于 goroutine / HTTP 中间件 / 服务器顶层做兜底,避免单点故障拖垮整个进程。
  • 不要跨 goroutine recover(每个 goroutine 需各自 defer recover)。

错误处理最佳实践

  1. 立即检查if err != nil 紧跟在可能出错的调用之后。
  2. 包装并附加上下文:返回错误时用自己的语义包装(%w),方便定位。
  3. 不要静默忽略:空 _ = err 会丢失信息,至少记录日志。
  4. 错误透传而非转换丢失:保留原始错误链便于 errors.Is/As 判断。
  5. 库代码返回 error,由调用方决定如何处理(重试、降级、返回用户友好信息等)。
go
// 推荐:包装 + 上下文
user, err := repo.FindUser(id)
if err != nil {
    return fmt.Errorf("查询用户 %d 失败: %w", id, err)
}

作者:yanshaodong