清晰的错误链能够保留失败原因和业务语境,让调用方做出正确决策。Go 的 error 是显式返回值,最佳实践不是在每一层重复打印,而是逐层补充必要信息,在边界处统一记录和转换。

核心原则

  • 使用 fmt.Errorf 与 %w 包装底层错误,在不丢失原因的前提下增加操作语境。
  • 用 errors.Is 判断哨兵错误、用 errors.As 获取具体错误类型,避免比较错误文本。
  • 定义少量稳定的领域错误,供上层决定重试、返回 404 或提示参数问题。
  • 只在能够处理错误的地方恢复;无法处理时立即返回,避免继续使用不完整状态。
  • 在 HTTP、消息消费等系统边界统一记录一次结构化日志,避免同一错误被重复打印。

推荐的实践步骤

仓储层可以把驱动错误包装为“查询用户”等操作语境,服务层把可识别的不存在错误转换为领域错误,接口层再映射为合适的状态码。日志应包含请求标识、操作名、耗时和完整错误链,但不要把数据库语句、令牌等敏感数据直接返回给客户端。

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

func findUser(id int64) error {
    if err := repo.Find(id); err != nil {
        return fmt.Errorf("find user %d: %w", id, err)
    }
    return nil
}

示例用于说明实现思路,实际项目还应结合所使用的框架版本、部署环境和业务约束进行调整。重要配置要进入版本管理,并在测试环境验证后再发布。

常见误区

  • 通过 err.Error() 字符串匹配错误类别,会在文案变化后悄然失效。
  • 每一层都记录日志再向上返回,会制造重复告警并增加排查噪声。
  • 直接把内部错误完整暴露给客户端,可能泄露表名、文件路径或依赖信息。

上线前检查清单

  • 确认“使用 fmt.Errorf 与 %w 包装底层错误,在不丢失原因的前提下增加操作语境”已经通过代码审查或运行验证。
  • 确认“用 errors.Is 判断哨兵错误、用 errors.As 获取具体错误类型,避免比较错误文本”已经通过代码审查或运行验证。
  • 确认“定义少量稳定的领域错误,供上层决定重试、返回 404 或提示参数问题”已经通过代码审查或运行验证。
  • 确认“只在能够处理错误的地方恢复;无法处理时立即返回,避免继续使用不完整状态”已经通过代码审查或运行验证。
  • 为失败路径、边界条件和回滚方案准备测试或演练记录。
  • 上线后观察错误率、延迟和资源消耗,确认变化符合预期。

总结

Go 错误处理最佳实践:包装、判断与日志边界的关键在于把隐含假设变成可执行的约束,并通过测试、监控和复盘持续验证。先从影响最大的真实场景开始,小步调整并保留回滚能力,通常比一次性大范围改造更安全,也更容易积累可复用的工程经验。