不能直接用 fmt.Errorf 返回业务错误,因其生成纯字符串 error,无 Code/HTTPStatus 字段,导致中间件无法识别、日志缺失来源;应使用实现 ErrorCoder 接口的自定义错误(如 *UserError)或 errcode 工厂函数(如 errcode.NewNotFound())。

为什么不能直接用 fmt.Errorf 返回业务错误
因为 fmt.Errorf 生成的是纯字符串 error,没有结构化字段,中间件拿不到 Code 或 HTTPStatus,日志里也查不到模块来源。比如写 return fmt.Errorf("user not found: %w", dbErr),上游就彻底丢失了「这是用户不存在」的语义,只剩一串文本。
真正该拦截和映射的,是实现了 ErrorCoder 接口的自定义错误类型(如 *UserError),它必须带 Code() int 和 HTTPStatus() int 方法。工厂函数创建时才做包装,确保 errors.Is 能精准判断、errors.Unwrap 可追溯原始错误。
- 禁止在 handler 里裸写
fmt.Errorf传给前端响应 - 所有业务错误必须经
errcode.NewNotFound()这类工厂函数生成 - 若需链式传递(service → handler),中间层可用
fmt.Errorf("xxx: %w", appErr),但最终响应前必须解包成*AppError
pkg/errcode 错误码常量怎么组织才不散落
硬编码 Code: 10001 在多个 handler 里出现,改一个漏一个;用字符串 map 做映射又没法导出、IDE 不提示、编译期不校验。
正确做法是新建 pkg/errcode/errcode.go,用 const + iota 定义可导出常量,并配套 Text(code int) string 方法:
立即学习“go语言免费学习笔记(深入)”;
const (
UserNotFound = iota + 10001 // 用户模块-未找到
UserInvalidMobile // 用户模块-手机号非法
OrderExpired // 订单模块-已过期
)
func Text(code int) string {
switch code {
case UserNotFound:
return "用户不存在"
case UserInvalidMobile:
return "手机号格式不合法"
default:
return "未知错误"
}
}
- 五位数字分层:前两位表模块(10=用户,20=订单),后三位表具体错误
- 每个常量必须带注释,说明触发场景和 HTTP 状态码建议值(如
UserNotFound对应404) -
Text()方法统一提供中文 msg,避免字符串字面量散落在各处
如何让 c.JSON 自动适配 HTTP 状态码与业务状态码
前端要的是「请求成功但业务失败」这种语义:HTTP 状态码是 200(网关、监控系统看),而 JSON 里的 code 是 10001(前端逻辑分支用)。混用会导致前端误判重试策略,或网关把 400 当作服务异常熔断。
响应结构体必须分离两层字段:
type Response struct {
Code int `json:"code"` // 业务码,0 表示成功
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
// 工厂函数示例
func Fail(c *gin.Context, httpStatus int, code int, msg string) {
c.JSON(httpStatus, Response{
Code: code,
Msg: msg,
})
}
-
Success()固定用http.StatusOK+Code: 0,不暴露 HTTP 层细节 -
Fail()第一个参数是httpStatus,第二个才是业务code,强制开发者思考「这个错误该不该被网关感知」 - 禁止在 handler 里手写
c.JSON(400, gin.H{"code": 10001})—— 必须走封装函数
自定义 validator 错误怎么映射到统一错误码
Gin 默认的 binding 错误返回的是英文字符串,比如 "Key: 'Register.Mobile' Error:Field validation for 'Mobile' failed on the 'required' tag",既不友好也不可控。
关键不是改 validator 本身,而是把它的错误转成可识别的 *AppError:
- 在
GetErrorMsg()函数里,根据validator.ValidationErrors的Field()和Tag()查request实现的GetMessages()映射表 - 查不到则 fallback 到
errcode.ParamInvalid,而不是拼字符串 - 最终调用
errcode.NewBadRequest(errcode.ParamInvalid),保证所有参数错误都走同一业务码路径
真正容易被忽略的是:validator 错误必须在中间件或 handler 入口就处理掉,不能让它流到下游再包装 —— 否则会多一层 error wrap,破坏 errors.Is(err, errcode.ParamInvalid) 的判断精度。


















