Gin 默认 panic 会返回空白500页面,因其内置 recovery 中间件仅捕获 panic 并打印日志,不调用 c.AbortWithStatusJSON(),也不触发自定义错误处理逻辑;必须禁用默认 recovery,手写中间件在 defer+recover 后统一返回结构化 JSON 响应。

为什么 Gin 的 panic 会直接返回 500 而不是你定义的错误页
Gin 默认遇到 panic 会调用 recovery.DefaultRecovery 中止请求并返回空白 500 页面,不走你写的中间件或 c.JSON。这是因为 panic 发生在路由 handler 执行过程中,而 Gin 的 recovery 中间件只是“打印堆栈 + 返回空响应”,并不触发你自定义的错误处理逻辑。
真正要接管异常,必须替换默认 recovery 行为,或者在它之后再加一层拦截——但更稳妥的做法是:自己写 recovery 中间件,并在其中统一构造错误响应。
- 不要依赖
gin.Default()自带的 recovery,改用gin.New()+ 手动注册中间件 - panic 后不能继续调用
c.Next(),必须在 recover 后立即写响应 - 注意:recover 只捕获当前 goroutine 的 panic,Gin 的 handler 是单 goroutine 执行,所以安全
如何编写一个能返回 JSON 错误的自定义 recovery 中间件
核心是用 defer + recover() 捕获 panic,然后根据 error 类型决定响应格式。别直接用 fmt.Sprintf("%v", err) 输出原始 panic 信息——生产环境必须脱敏。
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
var errMsg string
switch e := err.(type) {
case string:
errMsg = "internal server error"
case error:
errMsg = "internal server error"
default:
errMsg = "internal server error"
}
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": errMsg,
"data": nil,
})
}
}()
c.Next()
}
}
- 务必调用
c.AbortWithStatusJSON(),而不是c.JSON()+c.Abort(),否则可能被后续中间件再次处理 - error 类型判断只是为了避免 panic 时传入非 error 类型(比如直接
panic("xxx")),实际中建议统一用自定义 error 类型 - 如果想记录日志,把
log.Printf(...)放在c.AbortWithStatusJSON()前,且避免打印敏感字段(如密码、token)
如何让业务代码主动触发统一错误响应而不 panic
panic 应该只用于真正不可恢复的程序错误(如空指针解引用),业务错误(参数校验失败、DB 查询不到)必须显式返回,由上层统一包装。Gin 本身不提供“全局错误转响应”机制,得靠约定 + 中间件配合。
立即学习“go语言免费学习笔记(深入)”;
推荐做法:定义一个 ErrorResponse 结构体,在 handler 里用 return 提前退出,再用中间件检查是否已写响应。
- 在 handler 开头声明
var err error,校验失败时赋值,最后统一判断:if err != nil { c.Error(err); c.Abort(); return } - 或使用
c.Set("error", err)+ 后置中间件读取并响应(但要注意 c.Error() 是 Gin 内置错误收集,仅用于日志,不自动响应) - 更干净的方式:封装一个
SendError(c *gin.Context, code int, msg string)工具函数,所有业务错误都走它
为什么自定义 error 类型 + errors.Is 判断比字符串匹配更可靠
当你要区分不同错误类型做差异化响应(比如 DB not found 返回 404,权限不足返回 403),靠 err.Error() 包含某个字符串极易误判。Go 1.13+ 的 errors.Is 和 errors.As 是标准解法。
var (
ErrNotFound = errors.New("record not found")
ErrForbidden = errors.New("permission denied")
)
// handler 中
if errors.Is(err, ErrNotFound) {
c.AbortWithStatusJSON(404, gin.H{"code": 404, "msg": "resource not found"})
return
}
- 避免用
strings.Contains(err.Error(), "not found")—— 翻译、日志修饰、嵌套 error 都会让匹配失效 - 数据库驱动(如 gorm)多数已支持返回标准 error,可直接用
errors.Is(err, gorm.ErrRecordNotFound) - 第三方库若没实现
Is()方法,可用errors.As()尝试转换为具体类型再判断
真正难的不是捕获 panic,而是让 panic 少发生;不是写一堆 if-else 判断错误字符串,而是从设计上让错误可识别、可传递、可分类。业务逻辑里的错误路径,比 recovery 中间件更值得花时间理清楚。


















