📌 适用:Gin 新手|后端入门者|被
panic: runtime error吓醒过的打工人🕒 当前时间:2025年12月12日 · 周五 · 宜:写兜底逻辑,忌:裸奔上线
🤯 问题来了:Gin 默认不"兜底"
你写了个 /pay 接口:
go
func payHandler(c *gin.Context) {
var req struct{ UserID int }
c.ShouldBindJSON(&req)
// 假设数据库查用户......然后直接用了 req.UserID,但前端传了空 body
name := getUserByID(req.UserID).Name // 💥 req.UserID = 0 → 查不到 → .Name 空指针!
c.JSON(200, gin.H{"msg": "Hello " + name})
}
用户一调------
❌ panic: runtime error: invalid memory address or nil pointer dereference
服务直接挂掉,K8s 重启三连,监控告警轰炸全家群......
🙃 就像你煮泡面忘关火------
锅烧穿了,楼道烟雾报警器响了,整栋楼以为着火了......
其实你只需要一个「定时提醒器」+「自动关火开关」 🔥→✅
Gin 的「自动关火开关」,叫:全局异常中间件!
🛠 解决方案:用 Recover 中间件 + 自定义 panic 捕获
Gin 内置了一个 Recovery() 中间件------但它只打印日志,默认返回空白 500 (用户看到:{"error": "Internal Server Error"},一脸懵)。
我们要的是:
✅ 捕获 panic
✅ 记录日志(含堆栈!)
✅ 返回友好 JSON
✅ 不让服务挂掉
✅ Step 1:写一个「温柔型 Recovery」中间件
go
// middleware/recovery.go
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// Recovery 中间件:捕获 panic,返回友好错误,不崩服务!
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 📝 1. 记录关键信息(别漏了堆栈!debug.Stack() 是宝藏!)
stack := string(debug.Stack())
fmt.Printf("[PANIC] err=%v\n%s\n", err, stack)
// 🛡 2. 防止重复写响应(万一中间件链里多次 panic?)
if c.Writer.Written() {
return
}
// 💬 3. 返回「人话版」错误(生产环境别暴露堆栈!)
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "哎呀~服务器打了个小盹 😴",
"tip": "技术小哥已提着咖啡冲向工位,稍等哈~",
// ⚠️ 开发环境可加:"stack": stack (仅限 dev!)
})
}
}()
// 🚀 正常执行后续 handler
c.Next()
}
}
💡 小知识:
debug.Stack()能拿到 panic 发生时的完整调用栈------调试神器!但千万不能返回给前端(信息泄露风险!)
✅ Step 2:注册到 Gin Engine
go
// main.go
package main
import (
"github.com/gin-gonic/gin"
"your-project/middleware" // ← 上面的 Recovery 在这儿
)
func main() {
r := gin.New() // ⚠️ 用 gin.New() 而非 gin.Default(),避免默认日志干扰
// 🔑 关键:把 Recovery 中间件「挂」到最前面!
r.Use(middleware.Recovery())
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "我活着!"})
})
r.GET("/oops", func(c *gin.Context) {
// 模拟空指针 panic
var p *string
_ = *p // 💥 BOOM!
})
r.Run(":8080")
}
✅ 效果:
访问 /oops → 不再挂服务 → 返回:
json
{
"code": 500,
"message": "哎呀~服务器打了个小盹 😴",
"tip": "技术小哥已提着咖啡冲向工位,稍等哈~"
}
🎯 进阶:自定义业务异常(比如"余额不足")
Gin 没有像 FastAPI 那样的 @exception_handler 装饰器,但我们能自己造轮子------组合「自定义 error」+「统一错误响应中间件」。
Step 1:定义业务错误类型
go
// errors/errors.go
package errors
import "fmt"
type BizError struct {
Code int
Message string
}
func (e *BizError) Error() string {
return fmt.Sprintf("BizError[%d]: %s", e.Code, e.Message)
}
// 快捷构造函数
func NewBizError(code int, msg string) *BizError {
return &BizError{Code: code, Message: msg}
}
// 常用错误预定义
var (
ErrInsufficientBalance = NewBizError(1001, "余额不足,请充值~ 💸")
ErrUserNotFound = NewBizError(1002, "用户走丢了 🕵️♂️")
)
Step 2:写一个「BizError 拦截器」中间件
go
// middleware/biz_error.go
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"your-project/errors"
)
// BizErrorMiddleware:专门处理 *errors.BizError
func BizErrorMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // 先执行 handler
// 检查 handler 是否通过 c.Error() 设置了错误
errs := c.Errors
if len(errs) > 0 {
for _, e := range errs {
if bizErr, ok := e.Err.(*errors.BizError); ok {
c.JSON(http.StatusOK, gin.H{
"code": bizErr.Code,
"message": bizErr.Message,
})
c.Abort() // 不再走后续中间件
return
}
}
}
}
}
🔔 注意:Gin 里推荐用
c.Error(err)而非直接 panic 或 return error ------ 它会把错误存进c.Errors,供中间件统一处理!
Step 3:在 Handler 中「抛」业务错误
go
func buyCoffee(c *gin.Context) {
balance := 3.5
price := 15.0
if balance < price {
// ✅ 正确姿势:用 c.Error + Abort()
c.Error(errors.ErrInsufficientBalance)
c.Abort()
return
}
c.JSON(200, gin.H{"msg": "☕ 已下单!老板说送你一块曲奇~"})
}
Step 4:注册中间件(顺序很重要!)
go
r := gin.New()
r.Use(
middleware.Recovery(), // 兜底 panic
middleware.BizErrorMiddleware(), // 拦截业务错误
)
✅ 访问 /buy → 返回:
json
{
"code": 1001,
"message": "余额不足,请充值~ 💸"
}
🧩 对比总结:Gin 的两种异常处理套路
| 类型 | 触发方式 | 适用场景 | 是否中断请求 |
|---|---|---|---|
panic + Recovery() |
panic(xxx) 或运行时错误 |
未预期崩溃(空指针、越界等) | ✅ 中断,但服务不挂 |
c.Error(err) + 自定义中间件 |
主动调用 c.Error(myErr) |
业务逻辑错误(参数错、权限不足等) | ✅ 可控中断(配合 c.Abort()) |
📝 最佳实践:
- 业务错误 → 用
c.Error(err)+ 自定义中间件- 程序 Bug → 靠
Recovery()兜底 + 日志告警 + 快速修复- 永远不要让 panic 裸奔上线!