必须用 defer + recover 拦截 panic,且 defer 必须紧邻 c.Next() 前声明;recover 仅捕获 panic,不处理业务错误;需配合 c.AbortWithStatusJSON() 和 !c.Writer.Written() 判断,避免重复写响应;生产环境堆栈须脱敏,业务错误应统一通过 error 返回并由中间件集中处理。

panic 必须用 defer + recover 拦,但不能只靠它
Go 里没有 try-catch,recover 是唯一能“接住” panic 的方式,但它只在当前 goroutine 的 defer 函数中有效,且必须紧挨着 c.Next() 写。常见错误是把 defer 放在 c.Next() 后面,或包在另一层函数里——这时 recover 根本不会执行。
更关键的是:recover 只管 panic,不管业务错误。你 return errors.New("invalid token"),它完全无感;Gin 的 recovery.Default() 中间件也只打印日志、返回空白 500,不走你的错误响应逻辑。
-
defer func() { if err := recover(); err != nil { ... } }()必须直接写在c.Next()前 - panic 后要先检查
!c.Writer.Written(),再调c.AbortWithStatusJSON(),否则可能触发http: superfluous response.WriteHeader call - 别把堆栈信息(
debug.Stack())直接返回给前端,生产环境必须脱敏
Gin 中间件里拦截失败后必须 c.Abort() + return
写了个权限校验中间件,发现 403 返回了,但数据库还是被改了——大概率漏了 c.Abort() 或没 return。Gin 的 c.Abort() 只阻止后续中间件执行,不终止当前函数;漏掉 return,c.Next() 仍会往下跑,业务 handler 照常执行。
- 正确姿势:
c.JSON(403, gin.H{"msg": "forbidden"}); c.Abort(); return - 静态资源路径(比如
/assets/)也要过同一套拦截逻辑,否则 JS 文件可能泄露接口路径 - 别在中间件里
defer关闭 DB 连接之类资源,应绑定到c.Request.Context()生命周期
业务 error 要统一出口,不能散落在各 handler 里
每个 handler 都自己写 c.JSON(400, ...),结果错误码不统一、字段名不一致、日志格式五花八门。真正该拦截的,是显式返回的 error,不是 panic。
立即学习“go语言免费学习笔记(深入)”;
推荐做法:所有 handler 函数签名改为 func(c *gin.Context) error,中间件统一捕获并响应。Gin 不强制这个模式,得自己搭链路。
- 定义结构体如
type AppError struct { Code int; Msg string },预置ErrInvalidInput等变量 - 中间件里用
c.Set("app_error", err)传递,别依赖c.Errors(那是 Gin 内部调试用的) - 错误中间件注册顺序必须在自定义 recovery 之后,否则 panic 会提前中断流程
真实 IP 拦截前,先搞定 X-Forwarded-For 解析
c.ClientIP() 在线上基本不可用,Nginx/Traefik/CDN 会把真实 IP 放在 X-Forwarded-For 或 X-Real-IP 头里,而默认只信任回环网段,结果全是 127.0.0.1 或内网地址。
- 必须调
engine.SetTrustedProxies([]string{"192.168.0.0/16", "203.208.60.0/24"})显式配置可信代理段 - 若 Nginx 已设
proxy_set_header X-Real-IP $remote_addr,优先读c.Request.Header.Get("X-Real-IP") - 多层代理时,取
X-Forwarded-For最左非信任地址,不是最右——攻击者可伪造尾部 IP
拦截逻辑本身不难,难的是每一步都得做对:可信代理没配,等于把伪造头当真;c.Abort() 漏了,等于白拦;return 忘了,业务照样跑。这些点串起来,才是“优雅”的实际含义。


















