gin 里 error 处理与 c.Errors 机制
结论先行
gin 的 handler 签名没有返回值,错误不能“return 出去”,统一走 c.Errors:用 c.Error(err) 把错误追加到请求的错误列表,由错误处理中间件在 c.Next() 返回后统一读取、打日志或生成响应。c.AbortWithError(code, err) 则是在追加错误的同时中止 handler 链。
机制要点
| 概念 | 说明 |
|---|---|
| c.Errors | 类型 []*Error,保存本请求收集到的错误(实现 error 接口,可 String()) |
| c.Error(err) | 追加一个错误;不中止链、不写响应 |
| c.AbortWithError(code, err) | 追加错误 + Abort + 写状态码 |
| Error 类型 | 含 Err、Type(ErrorTypeBind/Private/Public…)、Meta,可分类过滤 |
| c.Errors.ByType(t) / Last() | 按类型筛选 / 取最后一个错误 |
推荐实践(集中式错误处理)
- 业务 handler / 绑定失败处:
c.Error(err)(可加 Meta 带上下文),必要时 Abort。 - 注册一个错误中间件放在链的外层:
c.Next()之后遍历c.Errors,统一记日志、按错误类型映射状态码与响应体。 - 对外只暴露安全信息,内部细节进日志——避免把 SQL 错误直接吐给客户端。
代码示例
// 统一错误处理中间件(放最外层)
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // 先跑完业务链
if len(c.Errors) == 0 {
return
}
var first = c.Errors[0]
status := http.StatusInternalServerError
if first.Type == gin.ErrorTypeBind {
status = http.StatusBadRequest
}
log.Printf("[err] path=%s err=%v meta=%v", c.Request.URL.Path, first.Err, first.Meta)
if !c.Writer.Written() { // 业务方还没写响应才接管
c.AbortWithStatusJSON(status, gin.H{"error": "request failed"})
}
}
}
// 业务方用法
if err := svc.Create(c, req); err != nil {
_ = c.Error(err).SetMeta("create_user") // 收集,让统一中间件兜底
c.Abort()
return
}
常见追问 / 记忆点
- 追问:c.Error 和直接返回 error 的区别?答:gin handler 无返回值,错误必须挂到请求上下文;中间件在链结束后统一可见、可分类、可打日志,天然支持“一处定义响应格式”。
- 追问:绑定失败的典型错误类型?答:ErrorTypeBind,可单独过滤做 400 提示。
- 追问:错误处理中间件放哪层?答:尽量靠外(紧跟 Recovery 内侧),才能兜住组内所有路由的错误。
- 记忆点:收集(c.Error)→ Next 后统一读取(c.Errors)→ 分类映射响应;响应只写一次,别重复。