应统一用含code、message、data字段的结构体返回错误,配合abortwithstatusjson、自定义recovery中间件、auth中间件及requestid透传,确保前后端错误处理一致且安全。

Gin 中 c.JSON 返回错误时前端拿不到 message 字段?
默认用 c.JSON(400, gin.H{"error": "参数缺失"}) 看似合理,但前端常抱怨“错误信息不统一”“status 字段没嵌套”“HTTP 状态码和 body 里的 code 对不上”。根本原因是没约定结构,也忽略了 Gin 的 c.AbortWithStatusJSON 和中间件拦截能力。
实际应统一用带 code、message、data 的结构体返回,例如:
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
再封装一个 SendError 工具函数,避免每个 handler 里重复写 c.JSON(status, Response{...})。
- HTTP 状态码(如 400)应反映真实语义,
code字段(如 1001)用于业务错误分类,二者不能混用 - 不要在
message里拼接用户输入,防止 XSS;敏感错误(如数据库连接失败)应降级为通用提示 - 若用
c.Abort()后再c.JSON(),必须配c.AbortWithStatusJSON(),否则可能触发多次写 response 的 panic
Gin 中如何拦截 panic 并转成标准错误响应?
Gin 默认 panic 会返回 500 页面(HTML),前端拿到的是空响应或 HTML 文本。必须用 recovery 中间件捕获,并主动调用 c.AbortWithStatusJSON。
别直接复用官方 gin.Recovery(),它只打日志不返回 JSON。自己写一个:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.AbortWithStatusJSON(500, Response{
Code: 5000,
Message: "服务内部错误",
Data: nil,
})
}
}()
c.Next()
}
}
- panic 里包含的原始 error(如
fmt.Errorf("db timeout: %w", err))不应直接暴露给前端,需记录到日志系统,但响应中只返回泛化提示 - 该中间件必须放在
router.Use()最前面,否则 panic 可能被上游中间件吞掉 - 若用了第三方日志库(如 zap),在 defer 里补上
logger.Error("panic recovered", zap.Any("err", err))
前端请求 401 时 Gin 怎么返回登录过期提示而不跳转?
前后端分离场景下,c.Redirect() 或 c.HTML() 完全失效。Auth 中间件校验 token 失败时,必须明确返回 JSON 错误,且 code 区分于普通业务错误(如设为 4010)。
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, Response{
Code: 4010,
Message: "登录已过期,请重新登录",
Data: nil,
})
return
}
// ... 解析 token 逻辑
if !valid {
c.AbortWithStatusJSON(401, Response{
Code: 4010,
Message: "登录已过期,请重新登录",
Data: nil,
})
return
}
c.Next()
}
}
- 不要用
http.StatusUnauthorized常量代替字面量 401,Go 里它只是 int 别名,可读性差 - 前端需约定识别
code === 4010触发登出流程,而非只看 HTTP 状态码(因为某些代理会重写状态码) - 若使用 JWT,注意
exp校验失败和签名无效应返回不同code(如 4011 vs 4012),方便前端区分处理
Gin 错误响应里要不要塞 trace_id?
要,但别塞在 message 里。trace_id 是调试线索,不是用户提示,应放在响应头或独立字段,且仅在非生产环境透出完整值。
推荐方案:在全局中间件中生成 X-Request-ID,并在错误响应的 data 中有条件地附带:
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
c.Set("request_id", id)
c.Header("X-Request-ID", id)
c.Next()
}
}
然后在 SendError 函数里判断环境:
if gin.Mode() != gin.ReleaseMode {
resp.Data = map[string]string{"request_id": c.GetString("request_id")}
}
- 生产环境绝对不要把 trace_id 拼进
message,否则前端弹窗会显示一串 UUID,影响体验 - 如果用了 OpenTelemetry,
request_id应与 trace ID 对齐,而不是另起一套 - 前端需在请求头带上
X-Request-ID(尤其重试时),后端优先取该值,避免同个请求多个 ID
code 字段做分支,而不是只看 HTTP 状态码;以及日志里有没有把 request_id 和错误堆栈对齐。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










