Day 24|错误处理最佳实践与工程规范[Go 语言 30 天系统学习计划]

学习目标

  • 建立一致、可维护的错误处理规范
  • 明确 error 与 panic 的工程边界
  • 掌握错误包装、透传与日志记录的配合方式
  • 设计“对调用方友好”的错误返回

一、为什么错误处理是工程能力的分水岭

在 Go 中,错误处理不是语法糖,而是:

  • 显式的控制流
  • 接口设计的一部分
  • 系统稳定性的基础

很多项目的问题不是“有没有错误处理”,而是:

  • 错误信息混乱
  • 错误被吞掉
  • 日志与错误职责不清

工程认知:错误处理的质量,直接决定系统的可维护性。


二、Go 错误处理的基本原则

原则 1:错误是值(error is a value)

  • error 是普通返回值
  • 可以比较、传递、包装

原则 2:谁能处理,谁处理

  • 当前层能解决 → 处理
  • 当前层不能解决 → 返回

原则 3:不要忽略错误

_, _ = io.Copy(dst, src) // ❌

忽略错误,等于制造隐患。


三、error 与 panic 的工程边界

使用 error 的场景:

  • 业务错误(参数非法、资源不存在)
  • 外部依赖失败(IO、网络、DB)
  • 可预期、可恢复的情况

使用 panic 的场景:

  • 程序不变量被破坏
  • 逻辑错误(理论上不应发生)
  • 启动阶段的致命配置错误

工程结论:panic 是“兜底机制”,不是业务流程。


四、错误包装(wrapping)的工程规范

推荐使用 Go 1.13+ 的错误包装:

return fmt.Errorf("open config failed: %w", err)

为什么要包装错误:

  • 保留底层错误信息
  • 增加当前层的业务语义
  • 支持 errors.Is / errors.As 判断

工程规范:

  • 每一层只加一层语义
  • 不要重复包装同一错误

五、错误判断:Is / As,而不是字符串

错误示例:

if strings.Contains(err.Error(), "timeout") {
    // ❌
}

正确方式:

if errors.Is(err, context.DeadlineExceeded) {
    // ✅
}

工程结论:错误判断必须基于类型或哨兵值,而不是文案。


六、哨兵错误 vs 自定义错误类型

哨兵错误(Sentinel Error):

var ErrNotFound = errors.New("not found")

适用场景:

  • 错误种类有限
  • 只关心“是什么错”

自定义错误类型:

type PermissionError struct {
    UserID int
}

适用场景:

  • 需要携带上下文信息
  • 错误本身是领域模型的一部分

工程建议:能用哨兵,就不要过度设计类型。


七、错误与日志的职责分离

常见反模式:

  • 每一层都打日志
  • 日志与 error 内容重复

推荐做法:

  • 底层:返回 error,不打日志
  • 中层:包装 error,仍不打日志
  • 边界层(HTTP / CLI):记录日志

工程结论:日志只在“错误被最终消费的地方”记录。


八、对调用方友好的错误设计

一个好的错误应该:

  • 能被程序判断(Is / As)
  • 能被人理解(Error() 信息清晰)
  • 不泄露内部实现细节

HTTP 服务常见模式:

  • 内部 error → 映射为 HTTP 状态码
  • 对外返回稳定、简洁的错误信息

九、错误处理的工程模式总结

  • error 是控制流的一部分
  • 包装错误,而不是替换错误
  • 判断错误,而不是解析字符串
  • 日志与错误职责分离

工程结论:好的错误设计,能让系统“自己解释自己(self-explanatory)”。


十、错误处理总结

  • 错误处理不是样板代码
  • 是 API 设计的一部分
  • 也是系统稳定性的基础

一句话总结:

错误不是麻烦,是系统给你的信息。


预告:Day 25|项目结构与包设计规范

下一天将系统讲解:

  • Go 项目目录结构的主流方案
  • 如何设计 package 边界
  • 避免循环依赖
  • 写出“可演进”的工程结构

发表评论