Appearance
05-中间件:从入门到自定义
中间件(Middleware)是 Gin 框架中最核心、最强大的特性之一。它允许我们在请求到达最终的处理函数之前,或者响应返回客户端之前,插入自定义的逻辑。无论是日志记录、权限校验、限流熔断、还是链路追踪,都离不开中间件。本篇将从原理到实战,系统讲解 Gin 中间件的方方面面。
中间件是什么?Gin 中间件机制简介
在 Gin 中,中间件本质上是一个签名为 func(*gin.Context) 的函数,和普通的路由处理函数(Handler)完全相同。Gin 内部通过一个处理函数链(HandlersChain) 来管理所有要执行的函数:
- 每个请求到来时,Gin 会构建一个
gin.Context对象,并将该路由对应的处理函数链(包含全局中间件、分组中间件、路由自身处理器)按顺序执行。 - 通过
c.Next()可以把控制权交给链中的下一个处理函数;当下游函数全部执行完毕后,控制权会回到当前函数,继续执行c.Next()之后的代码。 - 通过
c.Abort()可以终止链的继续执行,常用于认证失败时直接返回。
这种机制类似于"洋葱模型":请求像穿过洋葱一层层向内,响应又一层层向外返回,每一层都可以在 c.Next() 前后做事情。
go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
// 一个最简单的中间件,演示洋葱模型
func OnionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("[1] 进入中间件,请求即将向下传递")
c.Next() // 把控制权交给下一个处理函数
fmt.Println("[3] 中间件恢复执行,请求已经返回")
}
}
func main() {
r := gin.New()
r.Use(OnionMiddleware())
r.GET("/", func(c *gin.Context) {
fmt.Println("[2] 真正的业务处理函数")
c.JSON(http.StatusOK, gin.H{"msg": "hello"})
})
r.Run(":8080")
}执行后控制台输出顺序为:
text
[1] 进入中间件,请求即将向下传递
[2] 真正的业务处理函数
[3] 中间件恢复执行,请求已经返回可以看到 c.Next() 实现了"前→后→前"的洋葱结构。
内置中间件:Logger、Recovery、CORS
Gin 自带了几个常用中间件,可以直接使用:
| 中间件 | 作用 |
|---|---|
gin.Logger() | 在控制台输出请求日志(方法、路径、状态码、耗时) |
gin.Recovery() | 捕获 panic,返回 500,避免进程崩溃 |
gin.BasicAuth() | HTTP Basic 认证 |
gin.LoggerWithConfig() | 可定制的日志中间件 |
使用 gin.Default() 创建的引擎默认就启用了 Logger 和 Recovery。
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// gin.Default() 等价于 gin.New() + gin.Logger() + gin.Recovery()
r := gin.Default()
r.GET("/panic-demo", func(c *gin.Context) {
// 故意 panic,Recovery 会捕获并返回 500
panic("something went wrong")
})
r.GET("/ok", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
})
r.Run(":8080")
}如果使用 gin.New(),则不会有任何默认中间件,需要手动添加。在生产环境中推荐使用 gin.New() 然后按需装配中间件,以便精细化控制。
CORS(跨域)中间件不在内置范围内,需要通过官方扩展库 github.com/gin-contrib/cors 引入:
go
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
r := gin.Default()
// 配置 CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://foo.com", "https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
r.GET("/api/data", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "cross origin ok"})
})
r.Run(":8080")
}使用中间件:全局中间件、路由级中间件、分组中间件
中间件可以挂载到不同层级,作用范围也不同:
1. 全局中间件
通过 r.Use() 注册,对所有路由生效。
go
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())2. 路由级中间件
在定义路由时,把中间件作为参数传入,仅对该路由生效。
go
r.GET("/admin", AuthMiddleware(), AdminHandler)3. 分组中间件
通过 Group 创建路由组时挂载,仅对该组内的路由生效。
下面示例同时演示三种用法:
go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func GlobalMW() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("全局中间件执行")
c.Next()
}
}
func GroupMW() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("分组中间件执行")
c.Next()
}
}
func RouteMW() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("路由级中间件执行")
c.Next()
}
}
func main() {
r := gin.New()
r.Use(GlobalMW()) // 全局
// 路由分组 + 分组中间件
api := r.Group("/api", GroupMW())
{
api.GET("/public", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "public"})
})
// 仅此路由生效
api.GET("/private", RouteMW(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "private"})
})
}
r.Run(":8080")
}访问 /api/private 时执行顺序为:GlobalMW → GroupMW → RouteMW → 业务处理。
中间件执行顺序:c.Next() 和 c.Abort()
理解 c.Next() 与 c.Abort() 是掌握 Gin 中间件的关键。
c.Next() 的原理
c.Next() 会暂停当前中间件,执行链中后续的所有处理函数,等它们执行完毕后再回到当前中间件继续向下执行。它的实现大致等价于:
go
// 伪代码
func (c *Context) Next() {
c.index++
for c.index < len(c.handlers) {
c.handlers[c.index](c)
c.index++
}
}c.Abort() 的作用
c.Abort() 会将 c.index 设置为一个很大的值,使后续的处理函数不再执行。通常配合 c.JSON() 在认证失败时直接返回响应。
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "Bearer secret" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return // 注意:Abort 之后仍然要 return,避免继续执行后续代码
}
c.Next()
}
}
func main() {
r := gin.New()
r.GET("/profile", AuthRequired(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"user": "alice"})
})
r.Run(":8080")
}重要提示:
c.Abort()只是阻止后续 handler 执行,但不会终止当前中间件函数本身的执行流。所以调用Abort()后一定要return,否则当前函数中后续代码仍会执行。
AbortWithStatus / AbortWithStatusJSON
Gin 提供了三个便捷方法,等价于 Abort() + Status() 等组合:
c.AbortWithStatus(code int)c.AbortWithStatusJSON(code int, jsonObj any)c.AbortWithError(code int, err error)
自定义中间件:基本写法
自定义中间件有两种常见写法:
写法一:闭包函数(推荐)
适合需要传入配置参数的场景。
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// 带参数的中间件
func DelayMiddleware(d time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
time.Sleep(d)
c.Next()
}
}
// 不带参数的中间件
func SimpleMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("fromMiddleware", "hello")
c.Next()
}
}
func main() {
r := gin.New()
r.Use(SimpleMiddleware())
r.Use(DelayMiddleware(10 * time.Millisecond))
r.GET("/", func(c *gin.Context) {
val, _ := c.Get("fromMiddleware")
c.JSON(http.StatusOK, gin.H{"fromMiddleware": val})
})
r.Run(":8080")
}写法二:直接函数
适合不需要参数的简单场景。
go
func SimpleMiddleware(c *gin.Context) {
c.Set("fromMiddleware", "hello")
c.Next()
}
// 注册时直接传入函数引用
r.GET("/", SimpleMiddleware, handler)推荐"闭包函数"写法,统一风格,便于将来扩展参数。
中间件传参:通过 c.Set/c.Get 传递数据
gin.Context 内部维护了一个 map[string]any,可以在中间件和业务处理函数之间传递数据。
| 方法 | 说明 |
|---|---|
c.Set(key, value) | 设置键值对 |
c.Get(key) | 获取值,返回 (value, exists) |
c.MustGet(key) | 获取值,不存在则 panic |
c.GetString(key) | 获取字符串 |
c.GetInt(key) | 获取整数 |
c.GetStringSlice(key) | 获取字符串切片 |
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 解析用户信息并放入上下文
func UserMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.GetHeader("X-User-Id")
if userID == "" {
userID = "anonymous"
}
c.Set("userID", userID)
c.Set("role", "admin")
c.Next()
}
}
func main() {
r := gin.New()
r.Use(UserMiddleware())
r.GET("/me", func(c *gin.Context) {
userID, _ := c.Get("userID")
role, _ := c.Get("role")
c.JSON(http.StatusOK, gin.H{
"userID": userID,
"role": role,
})
})
r.Run(":8080")
}中间件实战示例
下面通过 4 个实战示例深入理解中间件的威力。
示例1:请求日志中间件(记录请求方法、路径、耗时、状态码)
go
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
// 请求前
c.Next()
// 请求后
latency := time.Since(start)
status := c.Writer.Status()
clientIP := c.ClientIP()
log.Printf("[GIN] %3d | %13v | %15s | %-7s %s",
status, latency, clientIP, method, path)
}
}
func main() {
r := gin.New()
r.Use(LoggerMiddleware(), gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "pong"})
})
r.Run(":8080")
}请求 /ping 后,控制台输出类似:
text
[GIN] 200 | 23.456µs | 127.0.0.1 | GET /ping示例2:JWT 认证中间件(基础版)
这是一个基础版,完整版将在后续章节讲解。这里只演示如何从 Header 解析 Token 并校验。
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// 简单的 Token 校验函数(实际项目中应使用 JWT 库)
func validateToken(token string) (string, bool) {
// 这里用一个假 token 演示,实际中解析 JWT
if token == "valid-token-alice" {
return "alice", true
}
return "", false
}
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing auth header"})
return
}
// 期望格式:Bearer <token>
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid auth format"})
return
}
username, ok := validateToken(parts[1])
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
// 把用户信息放入上下文,供后续 handler 使用
c.Set("current_user", username)
c.Next()
}
}
func main() {
r := gin.New()
// 不需要认证的路由
r.GET("/public", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "public api"})
})
// 需要认证的路由
auth := r.Group("/auth", JWTAuthMiddleware())
{
auth.GET("/profile", func(c *gin.Context) {
user, _ := c.Get("current_user")
c.JSON(http.StatusOK, gin.H{"user": user})
})
auth.GET("/settings", func(c *gin.Context) {
user, _ := c.Get("current_user")
c.JSON(http.StatusOK, gin.H{"user": user, "settings": "..."})
})
}
r.Run(":8080")
}测试:
bash
# 无 Token
curl http://localhost:8080/auth/profile
# => {"error":"missing auth header"}
# 正确 Token
curl -H "Authorization: Bearer valid-token-alice" http://localhost:8080/auth/profile
# => {"user":"alice"}示例3:限流中间件(令牌桶算法)
令牌桶算法的核心:以固定速率向桶里放令牌,桶有上限;每个请求消耗一个令牌,没令牌就拒绝。
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"sync"
"time"
)
// TokenBucket 令牌桶
type TokenBucket struct {
rate float64 // 每秒生成的令牌数
capacity float64 // 桶容量
tokens float64 // 当前令牌数
lastUpdate time.Time // 上次更新时间
mu sync.Mutex // 互斥锁
}
func NewTokenBucket(rate, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity, // 初始放满
lastUpdate: time.Now(),
}
}
// Allow 尝试取一个令牌
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
// 按经过时间补充令牌
elapsed := now.Sub(tb.lastUpdate).Seconds()
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
tb.lastUpdate = now
if tb.tokens >= 1 {
tb.tokens -= 1
return true
}
return false
}
// RateLimitMiddleware 限流中间件
// rate: 每秒生成多少令牌;capacity: 桶容量(允许瞬时并发)
func RateLimitMiddleware(rate, capacity float64) gin.HandlerFunc {
bucket := NewTokenBucket(rate, capacity)
return func(c *gin.Context) {
if !bucket.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
return
}
c.Next()
}
}
func main() {
r := gin.New()
// 每秒 2 个令牌,桶容量 5
r.Use(RateLimitMiddleware(2, 5))
r.Get("/api", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
})
r.Run(":8080")
}上面是单机限流。分布式场景应使用 Redis + Lua 实现令牌桶,例如
github.com/ulule/limiter库。
注意上面有个故意写错的 r.Get 应该是 r.GET,正确版本:
go
r.GET("/api", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
})示例4:请求ID追踪中间件
在微服务环境下,给每个请求打上唯一 ID,可以方便日志聚合和链路追踪。
go
package main
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log"
"net/http"
)
const RequestIDKey = "X-Request-Id"
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 优先复用上游传入的 request id
rid := c.GetHeader(RequestIDKey)
if rid == "" {
rid = uuid.New().String()
}
// 同时存入 context 和响应头,方便客户端排查
c.Set(RequestIDKey, rid)
c.Header(RequestIDKey, rid)
c.Next()
}
}
func main() {
r := gin.New()
r.Use(RequestIDMiddleware())
r.GET("/ping", func(c *gin.Context) {
rid, _ := c.Get(RequestIDKey)
log.Printf("处理请求,request_id=%s", rid)
c.JSON(http.StatusOK, gin.H{"msg": "pong", "request_id": rid})
})
r.Run(":8080")
}执行 curl -i http://localhost:8080/ping 可以看到响应头中包含 X-Request-Id。
中间件组合最佳实践
1. 推荐的注册顺序
中间件的执行顺序就是注册顺序,因此顺序至关重要。推荐顺序:
text
Recovery ← 必须最先,确保 panic 能被捕获
↓
RequestID ← 尽早生成,让后续日志都能关联
↓
Logger ← 记录请求开始/结束
↓
CORS ← 跨域预检要在最外层响应
↓
RateLimit ← 限流,挡住恶意请求
↓
Auth ← 认证
↓
Business ← 业务路由go
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { c.Set("rid", "fake-id"); c.Next() }
}
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { c.Next() }
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-Token") == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"err": "no token"})
return
}
c.Next()
}
}
func RateLimitMiddleware(r, cap float64) gin.HandlerFunc {
return func(c *gin.Context) { c.Next() }
}
func main() {
r := gin.New()
// 严格按推荐顺序注册
r.Use(gin.Recovery())
r.Use(RequestIDMiddleware())
r.Use(LoggerMiddleware())
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"*"},
MaxAge: 12 * time.Hour,
}))
r.Use(RateLimitMiddleware(100, 200))
// 公开路由
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// 鉴权路由
auth := r.Group("/api", AuthMiddleware())
{
auth.GET("/me", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"user": "alice"})
})
}
r.Run(":8080")
}2. 中间件应该保持单一职责
不要把日志、限流、认证塞进同一个中间件。每个中间件只做一件事,便于复用和测试。
3. 注意 goroutine 安全
gin.Context 在请求结束后会被复用(通过 sync.Pool)。如果中间件里启动了新的 goroutine,不能把 c 传过去用,而应该用 c.Copy() 得到一个只读副本:
go
func AsyncMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// ❌ 错误:go func() { log.Println(c.Request.URL.Path) }()
// ✅ 正确:
cc := c.Copy()
go func() {
// 在异步任务中安全使用 cc
}()
c.Next()
}
}4. 避免在中间件中做重活
中间件位于请求主链路上,任何阻塞操作都会拖慢整个请求。耗时任务(如发邮件、调外部 API)应放到异步队列里。
5. 使用 c.AbortWithError 记录错误
go
c.AbortWithError(http.StatusBadRequest, errors.New("invalid params"))这会把错误记录到 c.Errors,便于统一错误处理中间件统一返回。
小结
本篇系统介绍了 Gin 中间件:
- 原理:中间件本质是
func(*gin.Context),通过处理函数链 +c.Next()实现洋葱模型。 - 内置:
gin.Logger()、gin.Recovery()是最常用的,gin.Default()已经包含。 - 三层作用域:全局中间件、分组中间件、路由级中间件,作用范围依次缩小。
- 核心 API:
c.Next()传递控制权,c.Abort()终止链路,c.Set/c.Get传递数据。 - 实战:日志、JWT 认证、令牌桶限流、请求 ID 追踪四个典型场景。
- 最佳实践:注意注册顺序、单一职责、goroutine 安全、避免阻塞。
掌握中间件之后,你就能把横切关注点(cross-cutting concerns)从业务代码中干净地剥离出来。下一篇我们将学习模板渲染与文件上传。