gorm 默认错误不能直接用作业务错误码,因其仅为标准库error接口实例,无业务语义字段、不可被errors.as()识别、不带http状态码或trace_id,须封装为首字母大写的bizerror结构体并实现is()方法以支持精准匹配与统一处理。

为什么 GORM 默认错误不能直接用作业务错误码
GORM 返回的 error(比如 gorm.ErrRecordNotFound 或底层驱动错误)只是标准库 error 接口实例,没有业务语义字段、无法被 errors.As() 精准识别、也不带 HTTP 状态码或 trace_id。你不能靠 strings.Contains(err.Error(), "not found") 做判断——线上日志一多,这种字符串匹配就失效;更没法统一映射成 404 并透出给前端。
定义可识别、可携带上下文的自定义错误结构体
必须用首字母大写的结构体实现 error 接口,并暴露字段供反射访问(否则 errors.As() 拿不到值)。推荐包含 Code(业务错误码)、HTTPStatus(映射后的状态码)、Msg 和可选的 TraceID:
type BizError struct {
Code string `json:"code"`
HTTPStatus int `json:"-"`
Msg string `json:"msg"`
TraceID string `json:"trace_id,omitempty"`
}
func (e *BizError) Error() string {
return e.Msg
}
func (e *BizError) Is(target error) bool {
t, ok := target.(*BizError)
if !ok {
return false
}
return e.Code == t.Code
}
- 不要把
HTTPStatus放进 JSON 响应体,它只用于中间件写入w.WriteHeader() -
Is()方法让多个不同实例(比如不同TraceID)能按Code语义相等,方便下游做统一降级 - 所有字段必须首字母大写,否则
errors.As()反射失败
GORM 查询中如何包装原生错误为 BizError
别在 DAO 层直接返回 gorm.ErrRecordNotFound,而要立即转成你的业务错误类型。尤其注意:GORM 的 First()、Take() 等方法在查不到时返回该错误,但 Find() 不会——它只在 DB 报错时才返回 error,查空切片是正常流程。
- 查单条且预期存在 → 用
First()+errors.Is(err, gorm.ErrRecordNotFound)判断,转成&BizError{Code: "USER_NOT_FOUND", HTTPStatus: http.StatusNotFound, Msg: "用户不存在"} - 查列表 → 不依赖 error 判断是否存在,而是检查
result.RowsAffected == 0或len(users) == 0,按需返回空数据或特定错误(如“搜索条件无匹配结果”是正常响应,不是错误) - DB 连接失败、超时等底层错误 → 用
errors.Unwrap()向上提取,最终包裹为&BizError{Code: "DB_UNAVAILABLE", HTTPStatus: http.StatusServiceUnavailable, Msg: "数据库暂时不可用"}
HTTP 中间件里统一处理 BizError 并写入状态码
不要在每个 handler 里手动调 w.WriteHeader() 和 json.Marshal()。用中间件拦截返回的 error,只对 *BizError 做结构化响应,其他未识别错误一律记日志并返回 500:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 包装 ResponseWriter 以捕获 WriteHeader 调用(可选)
wr := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wr, r)
if err, ok := r.Context().Value("bizErr").(error); ok {
var bizErr *BizError
if errors.As(err, &bizErr) {
w.WriteHeader(bizErr.HTTPStatus)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": bizErr.Code,
"msg": bizErr.Msg,
"data": nil,
})
return
}
}
})
}
关键点:GORM 错误本身不带 HTTP 语义,必须由你主动注入。最容易被忽略的是——忘记在 DAO 返回前就把原生错误转成 *BizError,导致中间件收不到可识别类型,最终全退化成裸 500,连错误分类都做不了。











