应定义带Code()和Message()方法的自定义错误类型,通过常量错误码、查表获取提示、映射HTTP状态码、统一构造函数和Handler拦截实现结构化错误处理。

直接用 errors.New 或 fmt.Errorf 返回错误,无法携带错误码、HTTP 状态码、可翻译消息等结构化信息,业务层和 HTTP 层都难以做可靠分类处理——这不是“够用”,而是埋坑。
定义带 Code 和 Message 的自定义错误类型
核心是让错误既满足 error 接口,又能暴露结构化字段。不要嵌入 error 字段来“继承”语义,而应显式组合:一个 Code() 方法返回整型错误码,一个 Message() 方法返回默认提示,一个可选的 Cause() 方法支持错误链追溯。
- 错误码必须是常量(如
ErrUserNotFound = 1001),不能拼字符串或 runtime 计算 -
Message()不该硬编码在结构体里,而应查表获取——避免改提示要动几十个实例 - 若需国际化,
Message()只返回 i18n key,由上层调用翻译器渲染 - 结构体字段别导出(小写),全部通过方法暴露,防止外部篡改内部状态
用映射表解耦错误码与 HTTP 状态码
同一类错误语义应共享 HTTP 状态码,但错误码本身必须唯一。比如所有参数校验失败(ErrInvalidParam、ErrMissingField)都映射到 400,但它们的业务码不能重复,也不能共用一个变量。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 建一张全局
map[ErrorCode]int表,如httpStatusMap[ErrUserNotFound] = 404 - 禁止在每个错误实例里写死
httpStatus字段——否则改状态码得改遍所有构造处 - 不要把 500 当垃圾桶:客户端靠状态码区分客户端错(4xx)和服务端错(5xx),混用会导致前端重试逻辑失效
- 预留扩展位:例如用户模块用
1000–1999,其中1000–1099预留作未来子场景(如登录相关)
用封装函数替代裸 new struct
手动 &BizError{Code: 1001, Message: "xxx"} 极易出错,且破坏一致性。所有错误创建必须走统一入口函数,内部完成查表、默认消息填充、元数据初始化。
- 提供
NewBizError(code ErrorCode):自动从errorCodeMsgMap拿默认提示,从httpStatusMap拿状态码 - 提供
NewBizErrorWithMeta(code ErrorCode, meta map[string]any):给日志/监控留钩子,比如记录"user_id": 123 - 不暴露结构体字面量初始化,连测试都应走构造函数——避免某天有人绕过逻辑直接 new
- 若用
go-extend/errno,优先用errno.New(1001, "用户不存在")而非手写结构体
HTTP handler 层统一拦截并响应错误
业务 handler 不该自己调 http.Error(),否则错误处理散落在各处,状态码、日志格式、JSON 结构全不一致。
- 定义
StatusError类型,包装原始 error + HTTP 状态码,实现Error()方法 - 封装
AppHandler类型,实现http.Handler接口,在ServeHTTP中统一检查返回值是否为*StatusError - 遇到
*StatusError就写对应状态码 + 标准 JSON 响应体(含code、message、request_id) - 对非
*StatusError的 panic 或未预期 error,统一 fallback 到500并打告警日志——别让它静默失败
最容易被忽略的是错误码的“不可变性”:一旦对外发布(尤其跨服务 API),哪怕提示写错了、分类不合理,也绝不能改含义或复用码值。宁可新增一个码,也不要动旧的——否则前端缓存、监控告警规则、文档全会崩。

















