Go错误处理工程化:从errors.Is到故障注入
背景与问题界定 在一次微服务调用链路的故障排查中,一个下游超时错误经过了四层服务传递后,最终在入口层捕获到的错误信息是"internal error"——原始的timeout信息和调用栈全部丢失。开发团队花了两个小时通过日志关联才定位到根因。这个场景在微服务架构中并不罕见:错误在传播过程中被多次包装、吞没或降级,最终只剩下一个对排查毫无帮助的通用错误。 Go 1.13引入的errors.Is/As/Unwrap机制为错误链提供了语言级支持,但在实际工程中,错误处理远不止于"判断错误类型"。它涉及错误分类(业务错误 vs 系统错误)、错误分级(致命 vs 可重试)、错误传播(跨服务边界时如何携带上下文)以及错误的可观测性(结构化日志和告警)。更进一步的工程化实践还包括故障注入测试——主动模拟各种错误场景来验证系统的容错能力。 目标拆解与工程约束 错误类型必须分层且可穷举:错误分为基础设施错误(网络超时、磁盘IO)、框架错误(认证失败、限流触发)和业务错误(余额不足、库存不足)。每一层需要有明确的错误码范围和类型定义,避免跨层混合。 错误传播过程必须保留完整链路信息:错误从发生点到最终处理点可能经过多个goroutine和多个服务。需要确保每次wrap都附带足够上下文(操作名称、参数摘要、时间戳),同时不泄露敏感信息。 错误处理策略必须可配置:同一个错误在不同上下文中可能有不同的处理方式——DB超时在读取缓存时可能降级,但在写主库时必须fail-fast。需要策略模式来分离"错误检测"和"错误响应"。 故障注入必须安全且可观测:故障注入测试需要在非生产环境执行,但配置的模拟故障类型和比例需要与生产一致。注入的故障必须是可控的:有开始时间、结束时间和影响范围。 方案设计 核心方案构建在"错误沙盒"(Error Sandbox)概念之上。错误不再只是充当if err != nil的判断条件,而是携带了结构化的元数据: type AppError struct { Code string // 错误码,如"DB_TIMEOUT" Message string // 人类可读的错误描述 Op string // 操作名称,如"CreateOrder" Kind Kind // 错误大类:System / Business / Security Severity Severity // 严重级别:Fatal / Error / Warning Retryable bool // 是否可重试 Stack string // 调用栈(仅在内部传播时保留) Source string // 产生错误的服务/组件名 Timestamp time.Time // 错误发生时间 WrapErr error // 被包装的原始错误 } 错误的构建和检查通过工厂方法和断言函数完成: // 创建错误 func NewDBTimeoutError(op string, cause error) *AppError { return &AppError{ Code: "DB_TIMEOUT", Op: op, Kind: System, Severity: Error, Retryable: true, Source: "order-db", WrapErr: cause, } } // 错误断言 func IsRetryable(err error) bool { var ae *AppError if errors.As(err, &ae) { return ae.Retryable } return false } 故障注入方面,实现了一个基于接口的故障注入器。通过编译期开关和环境变量控制,在测试和非生产环境主动注入预定义的故障模式: ...