Go错误码体系核心是自定义BizError类型+接口断言分层识别,HTTP状态码映射须与业务错误码解耦;BizError含Code、Message、Details字段,实现Error()和ErrorCode()方法,通过errors.As断言及查表法映射状态码,禁用字符串匹配。

Go 里错误码体系不是靠 panic 或字符串拼接撑起来的,核心是用自定义错误类型 + 接口断言做分层识别,HTTP 状态码映射必须和业务错误码解耦,否则一改全崩。
怎么定义带错误码的业务错误类型
Go 原生 error 是接口,不能直接塞字段。得自己实现一个结构体,同时满足 error 接口和携带 Code、Message 的能力。
常见错误是直接在 fmt.Errorf 里硬编码状态码,比如 fmt.Errorf("user not found: 404")——这会让下游无法安全判断错误类型,也切不断 HTTP 层逻辑。
- 定义一个结构体,字段包含
Code(int或枚举常量)、Message(string)、可选的Details(map[string]interface{}) - 实现
Error()方法,只返回Message,不暴露Code给日志或前端原始输出 - 加一个
ErrorCode()方法(返回int),供中间件或 handler 判断 - 所有业务错误都用这个类型构造,不用
errors.New或fmt.Errorf直接返回
示例:
type BizError struct {
Code int
Message string
Details map[string]interface{}
}
func (e *BizError) Error() string { return e.Message }
func (e *BizError) ErrorCode() int { return e.Code }
如何把业务错误码映射到 HTTP 状态码
映射关系不能写死在 handler 里,也不能靠 switch 每个 Code 去判——新增一个错误码就得改一堆地方。得用查表法,且表要集中管理。
立即学习“go语言免费学习笔记(深入)”;
容易踩的坑:把 400 和 404 都映射成 http.StatusBadRequest,或者把数据库超时错误(应是 503)错映成 400。
- 定义一个全局映射表,比如
var httpStatusMap = map[int]int{ErrUserNotFound: http.StatusNotFound, ErrInvalidParam: http.StatusBadRequest, ...} - handler 中统一用
errors.As尝试断言为*BizError,再查表取状态码 - 没匹配上的默认用
http.StatusInternalServerError,但要打日志告警——说明漏配了新错误码 - 不要在
BizError里直接存 HTTP 状态码,它属于传输层语义,和业务域无关
为什么不能用 error string 匹配来判断错误类型
字符串匹配看着快,实则脆弱:日志格式一调、i18n 加个翻译、甚至多加个空格,strings.Contains(err.Error(), "not found") 就失效。而且完全破坏了类型安全。
更严重的是,它让错误处理逻辑散落在各处,没法统一加监控、重试或降级。
- 必须用
errors.As或errors.Is做类型/值断言,这是 Go 1.13+ 错误链的标准做法 - 如果要用
errors.Is,就在BizError里实现Is(target error) bool方法,对比Code字段 - 第三方库如
github.com/pkg/errors已过时,原生errors包足够用,别引入额外依赖
HTTP middleware 怎么统一透出错误码和状态码
中间件是收口点,但很多人在这里又写一堆 if-else,或者把 Code 直接塞进 HTTP header,导致前端必须解析 header 才知道错在哪——这违背 RESTful 原则。
真正该做的,是保持 HTTP 状态码语义正确,错误详情走 JSON body,且字段名稳定(如 code、message、request_id)。
- 中间件里统一检查
err是否为*BizError,拿到Code和Message - 查
httpStatusMap得到状态码,写入w.WriteHeader() - 响应体固定结构:
{"code": 1002, "message": "参数格式错误", "request_id": "abc123"} - 不要把数据库错误细节(如 “pq: duplicate key”)直接吐给前端,
BizError构造时就该脱敏
复杂点在于错误嵌套:比如 service 层返回 &BizError{Code: ErrUserNotFound},dao 层又 wrap 成 fmt.Errorf("failed to get user: %w", err)。这时 errors.As 仍能穿透找到原始 *BizError,但 errors.Is 要配合自定义 Is 方法才可靠。


















