为什么 Go 的错误处理值得专门写一篇

Go 没有 Java 那样的 checked exception,也没有 Python 的 try/except 统一捕获。它把 error 当作普通的值返回,让错误处理变成显式的、散布在每一行调用之后的判断。这种设计常被新手诟病”满屏 if err != nil”,但用好了,它带来的可读性与可控性是异常机制难以比拟的。

一、error 是值,不是异常

在 Go 里,error 是一个内建接口:

type error interface {
    Error() string
}

任何实现了 Error() string 的类型都能当作错误。这意味着错误可以被比较、包装、携带上下文,而不只是抛出一个调用栈。最根本的工程纪律是:error 必须被处理或被显式忽略,绝不能随手丢弃。

二、哨兵错误与 errors.Is

标准库定义了一批可导出的”哨兵错误”(sentinel error),例如 io.EOF、sql.ErrNoRows。早期代码习惯用 == 比较:

if err == sql.ErrNoRows {
    // 没有记录
}

问题在于,一旦错误被 wrap(用 %w 包装),== 比较就会失效。正确做法是用 errors.Is,它会沿错误链逐层比对底层错误:

if errors.Is(err, sql.ErrNoRows) {
    // 正确处理包装后的错误
}

三、自定义错误类型与 errors.As

当错误需要携带结构化字段(如错误码、重试次数)时,应定义自定义类型并用 errors.As 提取:

type TimeoutError struct {
    Timeout time.Duration
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("timeout after %s", e.Timeout)
}

// 调用方
var te *TimeoutError
if errors.As(err, &te) {
    log.Printf("超时时长: %v", te.Timeout)
}

errors.As 同样会穿透包装层,比直接对 err 做类型断言稳健得多。

四、用 %w 包装错误链

Go 1.13 引入 %w,让你在传播错误时保留原始错误:

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

这样上层既能用 errors.Is/As 还原底层错误,又能在日志里看到完整的上下文链。注意:%w 每个格式化串只能用一次,且不要用 %v 代替——%v 会丢失可解包性。

五、panic 的正确边界

panic 不是异常的替代品。它只应用于真正不可恢复的程序错误(如初始化阶段配置缺失)。对外的边界(HTTP handler、goroutine 入口)应当 recover 并返回错误,而不是让进程崩溃。

经验法则:库函数返回 error,程序主流程在边界处兜底 panic。把 error 留给调用方决策,把 panic 留给真正不可恢复的场景。

六、可直接落地的实践清单

  • 不要用 _ = err 静默丢弃,至少打日志或加注释说明为何忽略。
  • 对外 API 返回的错误不要暴露内部实现细节(数据库字段名、路径)。
  • 错误文案用小写开头、不加句末标点,保持 wrap 后拼接自然。
  • 在顶层统一用 errors.Is/As 做分支判断,而非 ==。

错误处理没有银弹,但遵循”显式处理、链式包装、边界兜底”三原则,足以让 Go 项目在规模增长后仍保持错误可控。下一篇可以聊聊如何用结构化日志与错误链配合,形成可观测性闭环。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注