This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/middleware.md
T

10 KiB
Raw Blame History

tags, create time
tags create time
后端
Go
Gin
中间件
架构
2026-04-27 00:00

中间件完整机制

概述

Gin 的中间件是一个轻量但强大的抽象——本质就是一个 func(*gin.Context) 类型的函数,通过 c.Next() 决定是否把控制权交给下一个 handler。Gin 的中间件系统有三层作用域,可以精确控制作用范围。

思考题:Gin 的中间件和 Go 标准库 net/http 的 func(http.Handler) http.Handler 中间件模式有什么本质区别?Gin 的方案更简单还是更灵活?

正文

1. 中间件的本质

中间件的类型签名就是 gin.HandlerFunc:

type HandlerFunc func(*Context)

它接收一个 *gin.Context,可以:

  1. 前置处理(读取请求、校验、记录日志等)
  2. 调用 c.Next()(把控制权交给下一个 handler)
  3. 后置处理(修改响应、收集指标等)
func logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()           // 前置:记录开始时间

        c.Next()                      // ← 必须调用,把控制权交给下一个

        // 后置:请求结束后的处理
        latency := time.Since(start)
        log.Printf("[%d] %s %s — %v", c.Writer.Status(), c.Request.Method, c.Request.URL.Path, latency)
    }
}

2. 三级作用域

Gin 中间件可以在三个层级注册,形成作用域嵌套:

全局中间件(Engine 级)
├── 分组中间件(RouterGroup 级)
│   ├── 路由中间件(单个路由级)
│   └── 路由中间件
└── 分组中间件
    └── 路由中间件

执行顺序: 全局 → 分组 → 路由 → handler

r := gin.Default()

// 全局中间件 — 所有路由都经过
r.Use(globalMiddleware())

// 分组中间件 — 仅该分组下的路由经过
v1 := r.Group("/api/v1", v1Middleware())
{
    // 路由中间件 — 仅此路由经过
    v1.GET("/users", routeMiddleware(), listUsers)
    v1.GET("/posts", listPosts)  // 不经过 routeMiddleware
}

执行链路示意:

请求: GET /api/v1/users

全局中间件(前置处理)
  ↓
v1 分组中间件(前置处理)
  ↓
路由中间件(前置处理)
  ↓
handler: listUsers
  ↓
路由中间件(c.Next() 返回后的代码)
  ↓
v1 分组中间件(c.Next() 返回后的代码)
  ↓
全局中间件(c.Next() 返回后的代码)

关键理解: 中间件链是先入后出的——c.Next() 之前的代码按注册顺序执行,c.Next() 之后的代码按逆序执行。

// 验证执行顺序
r := gin.Default()

r.Use(func(c *gin.Context) {
    fmt.Println("1-before")
    c.Next()
    fmt.Println("1-after")
})

r.Use(func(c *gin.Context) {
    fmt.Println("2-before")
    c.Next()
    fmt.Println("2-after")
})

r.GET("/test", func(c *gin.Context) {
    fmt.Println("handler")
})

// 输出:
// 1-before
// 2-before
// handler
// 2-after
// 1-after

思考题:如果中间件 A 中调用了 c.Abort()(不调用 c.Next()),中间件 B 和 handler 还会执行吗?那 A 中 c.Abort() 之后的代码还会执行吗?

3. 中间件链的构成

当你调用 c.Next() 时,内部执行的是 c.handlers 切片:

// gin/context.go
func (c *Context) Next() {
    c.index++
    for ; c.index < int8(len(c.handlers)); c.index++ {
        c.handlers[c.index](c)
    }
}

c.handlers 的来源是 全局中间件 + 分组中间件 + 路由中间件 的拼接:

全局中间件: [Logger, Recovery]
v1 分组中间件: [V1Middleware]
路由中间件: [RouteMiddleware]
handler:    [listUsers]

c.handlers = [Logger, Recovery, V1Middleware, RouteMiddleware, listUsers]

注册顺序就是执行顺序:

r := gin.Default()                              // Logger, Recovery

v1 := r.Group("/api/v1", authMiddleware())      // Logger, Recovery, authMiddleware
{
    v1.GET("/users", corsMiddleware(), listUsers) // Logger, Recovery, authMiddleware, corsMiddleware, listUsers
}

4. 常用中间件实现

CORS 中间件

func cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.Request.Header.Get("Origin")
        if origin != "" {
            c.Header("Access-Control-Allow-Origin", origin)
            c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
            c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization,X-Token")
            c.Header("Access-Control-Max-Age", "86400")
            c.Header("Access-Control-Allow-Credentials", "true")
        }
        // 处理 OPTIONS 预检请求
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        c.Next()
    }
}

认证中间件(JWT)

func jwtAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
            c.Abort()
            return
        }

        claims, err := parseJWT(token)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
            c.Abort()
            return
        }

        // 把用户信息存入 Context,后续 handler 可直接获取
        c.Set("userID", claims.UserID)
        c.Set("role", claims.Role)
        c.Next()
    }
}

请求日志(结构化)

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

        c.Next()

        latency := time.Since(start)
        status := c.Writer.Status()

        log.WithFields(log.Fields{
            "method":     c.Request.Method,
            "path":       path,
            "query":      query,
            "status":     status,
            "latency_ms": latency.Milliseconds(),
            "client_ip":  c.ClientIP(),
        }).Info("request")
    }
}

请求 ID 中间件

const requestIDKey = "X-Request-ID"

func requestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        id := c.GetHeader(requestIDKey)
        if id == "" {
            id = uuid.New().String()
        }
        c.Set(requestIDKey, id)
        c.Header(requestIDKey, id)
        c.Next()
    }
}

提问: 上面的 JWT 中间件中,c.Set("userID", ...) 把用户信息存进了 Context。如果认证失败(c.Abort()),那这个 userID 还会被后面的 handler 读到吗?为什么?

5. 中间件中启动 Goroutine 的陷阱

这是 Gin 中间件最常见的坑:在中间件中启动 goroutine 后,goroutine 可能引用已释放的 Context。

错误写法:

func asyncProcessor() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // 危险!c.Next() 返回后,Context 可能已经被回收
        // 但 goroutine 还在运行,可能会 panic
        go func() {
            log.Printf("处理完成: %s", c.Request.URL.Path)  // ← c 可能已被 pool 回收
        }()
    }
}

正确写法:用 c.Copy() 创建独立副本

func safeAsyncProcessor() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // c.Copy() 创建请求副本,包含独立的 Request 和 Context
        // 注意:copy 中的 Writer 是无效的,只能读 Request 和 Context keys
        copy := c.Copy()
        go func() {
            // 安全的异步处理,只读取数据,不能写入响应
            log.Printf("异步处理完成: %s", copy.Request.URL.Path)
            userID := copy.GetString("userID")
            log.Printf("用户 %s 的请求已异步处理", userID)
        }()
    }
}

c.Copy() 的限制:

可以复制的 不可以复制的
c.Request(原始请求) c.Writer(ResponseWriter 无法复制)
c.Keys(已设置的键值对) c.Errors(错误链不能写)
c.Params(路径参数) 不能调用 c.JSON() 等写响应的方法
c.ClientIP() 不能修改请求体

核心规则: c.Copy() 出的 goroutine 只能读不能写。如果需要在异步中写数据,用数据库/队列等持久化方式,不要依赖 Context。

6. 跳过中间件

有时不想让特定路由经过某些中间件,有两种方式:

方式一:将中间件注册到子分组而非全局

r := gin.Default()

// 只挂载到 /api 分组
api := r.Group("/api", rateLimitMiddleware())
{
    api.GET("/public", publicHandler)   // 经过 rateLimit
    api.GET("/private", privateHandler) // 经过 rateLimit
}

r.GET("/health", healthHandler)  // 不经过 rateLimit

方式二:中间件内部判断跳过

func logging() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 跳过健康检查端点
        if c.Request.URL.Path == "/health" {
            c.Next()
            return
        }
        // 正常日志逻辑
        start := time.Now()
        c.Next()
        log.Printf("[%d] %s", c.Writer.Status(), time.Since(start))
    }
}

7. 中间件的常见应用场景

场景 方案
跨域处理 CORS 中间件
身份认证 JWT / Session 中间件
权限控制 RBAC 中间件
请求限流 Token Bucket / 滑动窗口中间件
请求日志 结构化日志中间件
请求追踪 RequestID 中间件
异常恢复 gin.Recovery()
缓存 响应缓存中间件
数据预处理 数据注入中间件(如把数据库对象注入 Context)

思考题:如果需要在多个分组之间共享中间件(比如 api/v1 和 api/v2 都需要 CORS),是把 CORS 注册到全局好,还是注册到各自分组好?为什么?

关联笔记

  • [[GIN/gin-architecture]] — 中间件链的底层执行机制
  • [[GIN/context-lifecycle]] — c.Copy() 的深拷贝原理
  • [[GIN/session-auth]] — 认证/授权中间件实战