Go Gin 全局异常处理:别让 panic 把你的服务“原地升天”

📌 适用: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 裸奔上线!

相关推荐
行百里er1 天前
2026:一名码农的“不靠谱”年度规划
后端·程序员·架构
计算机程序设计小李同学1 天前
基于SpringBoot的个性化穿搭推荐及交流平台
java·spring boot·后端
用户47949283569151 天前
同事一个比喻,让我搞懂了Docker和k8s的核心概念
前端·后端
li.wz1 天前
Spring Bean 生命周期解析
java·后端·spring
sanggou1 天前
【实战总结】Spring Boot 后端接口防抖详解与实现方案(含注解 + Redis)
spring boot·后端
Victor3561 天前
Hibernate(26)什么是Hibernate的透明持久化?
后端
盖世英雄酱581361 天前
不是所有的this调用会导致事务失效
java·后端
Victor3561 天前
Hibernate(25)Hibernate的批量操作是什么?
后端
Thetimezipsby1 天前
Go(GoLang)语言基础、知识速查
开发语言·后端·golang
为自己_带盐1 天前
从零开始玩转 Microsoft Agent Framework:我的 MAF 实践之旅-第二篇
后端·microsoft·ai·.net