错误码不统一的本质是未分层:业务、校验、基础设施错误混用,导致前端无法依赖HTTP状态码判断,日志缺失上下文,监控难以打标;必须按语义映射标准HTTP码(如404/400/503),所有错误实现StatusCode()接口,由统一中间件拦截处理,禁止handler内重复响应,且生产环境错误消息须脱敏。

Go 微服务里错误码不统一,本质不是“没写好”,而是没把错误分层——业务错误、校验错误、基础设施错误混在一起返回,导致前端无法靠 status 做判断,日志查不到上下文,监控打不了标。
HTTP 状态码必须语义化,不能全用 200 + code 字段
很多团队习惯返回 200 OK 再塞一个 {"code": 404, "msg": "user not found"},这会让前端 fetch 的 response.ok 永远为 true,重试逻辑失效,CDN 缓存策略错乱。
- 资源不存在 → 必须用
http.StatusNotFound(404),不是 200 - 参数校验失败 →
http.StatusBadRequest(400),不是 500 - 权限不足 →
http.StatusForbidden(403),不是 401(除非是鉴权环节) - 创建成功 →
http.StatusCreated(201),并设Locationheader - 删除成功且无响应体 →
http.StatusNoContent(204)
错误类型要分层,别让 service 层直接 throw error 到 handler
service 层抛出的 fmt.Errorf("failed to get user: %w", err) 会丢失原始错误类型,中间件没法区分是 DB 超时还是 ID 格式错。真正可维护的做法是定义领域错误类型:
-
ErrUserNotFound→ 映射到 404 -
ErrInvalidEmail→ 映射到 400 -
ErrDBTimeout→ 映射到 503(Service Unavailable),不是 500 - 所有错误都实现
interface{ StatusCode() int },让中间件统一调用
中间件统一拦截错误,禁止 handler 里反复写 c.JSON()
每个 handler 都写 if err != nil { c.JSON(...); return },既重复又容易漏掉状态码或 header 设置。应该用全局错误中间件:
立即学习“go语言免费学习笔记(深入)”;
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
if statusErr, ok := err.(StatusError); ok {
c.JSON(statusErr.StatusCode(), gin.H{"error": statusErr.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
}
}
- 注册时加在路由最外层:
r.Use(ErrorHandler()) - service 层只返回自定义错误,不调
c.JSON - 中间件里可统一加
X-Request-ID、记录日志、上报 metrics
错误消息要脱敏,生产环境禁用 Go error.Error() 直出
数据库连接失败时返回 "pq: dial tcp 10.0.1.5:5432: connect: connection refused",等于把内网地址和端口暴露给调用方。
- 开发环境可保留详细错误(用于 debug)
- 生产环境统一 fallback 到泛化提示:
"request failed, please try again later" - 敏感字段如 SQL、stack trace、路径、host 名必须过滤
- 建议用
errors.Is(err, ErrUserNotFound)判断,而不是字符串匹配
最难的不是定义多少个错误码,而是让每个错误从发生点(service)、透出点(handler)、落地点(middleware)都保持类型一致——否则加再多 code 字段也没法自动化处理。


















