Skip to content

20|Gin 中间件

管理 API 需要一些横切功能:记录每个请求的日志、捕获 panic 防止服务崩溃、计算请求耗时、添加 CORS 头、为每个请求注入唯一的追踪 ID。这些功能如果写在每个 handler 里,代码会大量重复。Gin 的中间件机制把这类横切逻辑抽出来,注册一次就能作用于所有路由。

一、中间件模式

Gin 的中间件是一个 gin.HandlerFunc,它在请求到达目标 handler 之前执行,调用 c.Next() 把控制权交给下一个中间件或 handler,等 handler 返回后再继续执行中间件的后续逻辑。

go
func LoggerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path

		// 调用下一个 handler
		c.Next()

		// handler 返回后继续执行
		cost := time.Since(start)
		status := c.Writer.Status()
		fmt.Printf("%s %d %v\n", path, status, cost)
	}
}

c.Next() 是关键。没有它,中间件会直接返回,后续的 handler 不会执行。c.Next() 之后的代码在 handler 处理完成后才执行,适合记录响应时间和状态码。

注册中间件:

go
r := gin.New()
r.Use(LoggerMiddleware())

gin.Default() 内部已经注册了日志和恢复中间件。如果不需要默认的彩色日志(比如生产环境用结构化日志),用 gin.New() 创建引擎,再手动注册需要的中间件。

二、内置中间件

Gin 提供了几个常用内置中间件:

中间件作用
gin.Logger()打印请求日志
gin.Recovery()捕获 panic,返回 500,防止服务崩溃
gin.BasicAuth()HTTP Basic 认证
go
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())

三、自定义中间件

请求 ID 注入

为每个请求生成唯一 ID,便于日志关联和故障排查:

go
func RequestIDMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		requestID := c.GetHeader("X-Request-ID")
		if requestID == "" {
			requestID = uuid.New().String()
		}
		c.Set("request_id", requestID)
		c.Writer.Header().Set("X-Request-ID", requestID)
		c.Next()
	}
}

c.Set 把数据存入 gin 的上下文,后续 handler 可以用 c.Get("request_id") 取出。如果客户端已经传了 X-Request-ID,保留客户端的值,实现链路追踪。

CORS 中间件

跨域资源共享,允许浏览器从不同域名访问 API:

go
func CORSMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}

		c.Next()
	}
}

c.AbortWithStatus 终止后续中间件和 handler 的执行,直接返回状态码。预检请求(OPTIONS)不需要走到业务 handler。

计时中间件

记录慢请求,用于性能监控:

go
func SlowLogMiddleware(threshold time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		cost := time.Since(start)
		if cost > threshold {
			fmt.Printf("slow request: %s %s %v\n", c.Request.Method, c.Request.URL.Path, cost)
		}
	}
}

注册:

go
r.Use(SlowLogMiddleware(500 * time.Millisecond))

四、中间件执行顺序

中间件按注册顺序执行,形成一条链:

go
r.Use(A())
r.Use(B())
r.Use(C())
r.GET("/hello", handler)

执行顺序:

A 前半 → B 前半 → C 前半 → handler → C 后半 → B 后半 → A 后半

这个顺序和洋葱模型一样,外层中间件包裹内层。日志中间件应该最早注册,这样它能记录到最完整的耗时;认证中间件应该在业务 handler 之前,但可以在日志之后。

路由组也可以单独注册中间件:

go
admin := r.Group("/admin")
admin.Use(AuthMiddleware())
{
	admin.GET("/routes", listRoutes)
}

AuthMiddleware 只作用于 /admin 下的路由,不影响其他路由。

五、在管理 API 中使用

go
func main() {
	r := gin.New()
	r.Use(RequestIDMiddleware())
	r.Use(LoggerMiddleware())
	r.Use(gin.Recovery())
	r.Use(CORSMiddleware())
	r.Use(SlowLogMiddleware(500 * time.Millisecond))

	admin := r.Group("/admin")
	// admin.Use(AuthMiddleware()) // 下一讲加入认证
	{
		admin.GET("/routes", listRoutes)
		admin.POST("/routes", addRoute)
	}

	r.Run(":8081")
}

六、常见错误

忘记调用 c.Next()

go
func BadMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("before")
		// 忘了 c.Next()
		fmt.Println("after")
	}
}

这个中间件会阻止后续 handler 执行,请求永远得不到响应。

在 c.Abort 之后还调用 c.Next()

go
func Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if !check(c) {
			c.AbortWithStatus(403)
		}
		c.Next() // 错误:Abort 之后不应该再 Next
	}
}

c.Abort 设置了 abort 标志,c.Next() 会检测到并跳过后续执行。但代码逻辑上容易让人困惑。正确做法是用 return

go
func Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if !check(c) {
			c.AbortWithStatus(403)
			return
		}
		c.Next()
	}
}

中间件里 panic 没有被恢复

如果自定义中间件里发生 panic,而 gin.Recovery() 没有注册,整个服务会崩溃。使用 gin.New() 时,记得手动注册 Recovery 中间件。