不能直接用 fmt.errorf 返回业务错误,因其生成的 error 是纯字符串,无结构化字段,中间件无法提取 code 或 httpstatus,日志中也查不到错误来源模块;必须使用实现 errorcoder 接口的自定义错误类型(如 usererror)并经工厂函数创建,底层 error 需 wrap 包装,确保 errors.is 精准判断且 code 与 httpstatus 解耦映射。

为什么不能直接用 fmt.Errorf 返回业务错误
因为 fmt.Errorf 生成的 error 是纯字符串,不带结构化字段,中间件无法提取 Code 或 HTTPStatus,日志里也查不到错误来源模块。一旦有人写 return fmt.Errorf("user not found: %w", dbErr),上游就彻底丢失了错误码语义。
真正要拦截和映射的,是实现了 ErrorCoder 接口的自定义错误类型,比如:
type UserError struct {
code int
msg string
httpStatus int
}
func (e *UserError) Code() int { return e.code }
func (e *UserError) Message() string { return e.msg }
func (e *UserError) HTTPStatus() int { return e.httpStatus }
- 所有业务错误必须通过工厂函数创建,例如
user.NewNotFoundErr(),禁止裸&UserError{...} - 底层依赖(如数据库、RPC)返回的 error 必须用
Wrap()包装,而不是直接return err -
errors.Is(err, user.ErrNotFound)能精准判断,但前提是ErrNotFound是变量而非常量数字
如何让 Code 和 HTTPStatus 解耦又可映射
错误码 Code 是业务唯一标识(如 10001 表示用户不存在),而 HTTPStatus 是协议层状态(如 404)。二者不能硬绑定,否则改状态码就得动所有错误定义。
推荐做法是建一张轻量映射表,在中间件序列化响应前做一次查表:
var statusCodeMap = map[int]int{
10001: 404, // user not found
10002: 400, // user param invalid
50001: 500, // system internal error
}
- 映射表只在初始化时加载一次,线程安全,不支持运行时修改
- 未显式映射的
Code默认 fallback 到500,避免因遗漏导致返回200错误体 - 不要把
HTTPStatus写死在错误结构体里——它属于传输层决策,不是错误本身的属性
Gin 中间件怎么统一捕获并转成 JSON 响应
别在每个 handler 里写 if err != nil { c.JSON(...); return }。Gin 的标准做法是让 handler 签名返回 error,再用中间件统一处理:
func RespMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
// 不推荐:c.Errors 是 gin 自己的错误栈,和业务 error 无关
}
// 正确方式:从 c.Get("err") 或自定义上下文字段取 error
if err, ok := c.Get("app_err").(error); ok && err != nil {
if coder, ok := err.(ErrorCoder); ok {
c.JSON(statusCodeMap[coder.Code()], gin.H{
"code": coder.Code(),
"msg": coder.Message(),
"trace_id": c.GetString("trace_id"),
})
return
}
// 非结构化 error,转为系统错误
c.JSON(500, gin.H{"code": 50001, "msg": "system error"})
}
}
}
- handler 函数需显式调用
c.Set("app_err", err),或更推荐:用闭包封装 handler,自动捕获返回值 - 不要依赖
defer + recover拦截业务错误——那是给 panic 用的,不是给return user.ErrNotFound用的 - 中间件里别忘了设置
c.Status(),否则默认是200,JSON 体对不上状态码会误导前端
错误信息怎么支持多语言但不拖慢请求
message 不该是固定字符串,而应是 key(如 "user_not_found"),运行时根据 Accept-Language 查表。但查 map 是 O(1),关键在初始化阶段别做 I/O 或网络请求。
典型做法:
var i18nMap = map[string]map[string]string{
"zh-CN": {
"user_not_found": "用户不存在",
"param_invalid": "参数格式错误",
},
"en-US": {
"user_not_found": "User not found",
"param_invalid": "Invalid parameter",
},
}
- 启动时一次性加载全部语言包到内存,不按需读文件或调 API
- 在
ErrorCoder.Message()方法里做 key 查表,传入当前请求的 locale(从 header 或 context 取) - 开发环境可强制设为
en-US,避免中文乱码干扰调试;上线后按 header 自动切换 - 如果某个 key 缺失,fallback 到 key 本身(如返回
"user_not_found"),而不是 panic 或空字符串
fmt.Errorf、不在 handler 里直接 c.JSON**。约束靠文档没用,得靠编译期检查(比如用 go vet 插件扫描裸 fmt.Errorf)、CI 拦截、以及错误构造函数命名足够醒目(比如叫 MustNewUserError)。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











