团队接了一个新项目,定代码规范的时候在 error 处理上吵了整整一个下午。这篇文章整理一下我们最终达成的共识,以及为什么。
先说反模式。 常见的两种流派都有问题:
- “warp 派”:每一层都
fmt.Errorf("xxx: %w", err)包一层,最后日志里出现五行重复的query user: query user: query user: record not found。信息量为零,日志长度翻倍。 - “裸传派”:err 原样往上传,顶层打日志的时候只知道出错了,不知道错在哪个业务环节。
我们现在的做法,说起来很朴素:
- 只有"当前层新增了语义"的时候才包装。 比如数据层返回
record not found,到 service 层要变成ErrUserNotFound,这是有语义提升的,值得包。纯粹透传的调用直接return err。 - 哨兵错误 + errors.Is 为主,自定义错误类型按需用。 90% 的场景一个
var ErrXxx = errors.New("xxx")就够了,别上来就定义结构体。真需要携带上下文(比如重试次数、限流详情)再上类型断言errors.As。 - 打日志和返回 error 只做一件事。 这是我们的铁律:中间层不要既打日志又往上返回,否则同一次错误在日志里出现 N 遍,定位的时候全是噪音。要么就地处理并打日志(返回 nil),要么不打日志继续上传。顶层(通常是 HTTP handler 或 cron 入口)统一打一次。
- 第三方库的 error 要在边界收口。 库的 error 类型不该泄漏到业务代码里,否则哪天换库就是全项目翻新。在 repository 层把 gorm 的错误翻译成领域错误,业务层只认识领域错误。
另外一个和 error 无关但经常一起争论的问题:panic 什么时候用。我们的边界也很清晰——只在程序初始化阶段(连不上数据库这种"起不来就没意义"的场景)panic,运行期的错误一律返回。以及一个共识:recover 只允许出现在最顶层的中间件里,业务代码里出现 recover 一律打回重写。
规范本身不重要,团队认账才重要。吵一下午是值得的。