Skip to content

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() 创建的引擎默认就启用了 LoggerRecovery

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 中间件:

  1. 原理:中间件本质是 func(*gin.Context),通过处理函数链 + c.Next() 实现洋葱模型。
  2. 内置gin.Logger()gin.Recovery() 是最常用的,gin.Default() 已经包含。
  3. 三层作用域:全局中间件、分组中间件、路由级中间件,作用范围依次缩小。
  4. 核心 APIc.Next() 传递控制权,c.Abort() 终止链路,c.Set/c.Get 传递数据。
  5. 实战:日志、JWT 认证、令牌桶限流、请求 ID 追踪四个典型场景。
  6. 最佳实践:注意注册顺序、单一职责、goroutine 安全、避免阻塞。

掌握中间件之后,你就能把横切关注点(cross-cutting concerns)从业务代码中干净地剥离出来。下一篇我们将学习模板渲染与文件上传。