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 裸奔上线!

相关推荐
爱吃KFC的大肥羊2 小时前
Redis 基础完全指南:从全局命令到五大数据结构
java·开发语言·数据库·c++·redis·后端
用户2190326527352 小时前
Spring Boot4.0整合RabbitMQ死信队列详解
java·后端
天天摸鱼的java工程师2 小时前
🚪单点登录实战:同端同账号互踢下线的最佳实践(Java 实现)
java·后端
小飞Coding2 小时前
Java堆外内存里的“密文”--从内存内容反推业务模块实战
jvm·后端
狂奔小菜鸡2 小时前
Day29 | Java集合框架之Map接口详解
java·后端·java ee
踏浪无痕2 小时前
告别手写 TraceId!Micrometer 链路追踪在 Spring Boot 中的落地实践
后端·spring cloud·架构
捧 花2 小时前
Go Web 中 WebSocket 原理与实战详解
网络·后端·websocket·网络协议·http·golang·web
serendipity_hky2 小时前
【SpringCloud | 第3篇】Sentinel 服务保护(限流、熔断降级)
java·后端·spring·spring cloud·微服务·sentinel
漂亮的小碎步丶2 小时前
【2】Spring Boot自动装配
java·spring boot·后端