学习目标
- 建立一致、可维护的错误处理规范
- 明确 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 边界
- 避免循环依赖
- 写出“可演进”的工程结构