跳到主要内容

中间件原理

问题

Go Web 框架中的中间件是什么?执行顺序是怎样的?

答案

中间件本质

中间件就是一个 gin.HandlerFunc,通过 c.Next() 控制执行流程:

执行流程:洋葱模型

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

// ① 请求前(洋葱外层 → 内层)
fmt.Printf("[开始] %s %s\n", c.Request.Method, path)

c.Next() // ② 执行下一个中间件或 Handler

// ③ 响应后(洋葱内层 → 外层)
latency := time.Since(start)
fmt.Printf("[完成] %s %s %d %v\n",
c.Request.Method, path, c.Writer.Status(), latency)
}
}

Gin 中间件链实现

// Gin 内部:HandlersChain 就是 []HandlerFunc
type HandlersChain []HandlerFunc

type Context struct {
handlers HandlersChain
index int8 // 当前执行到第几个 handler
}

// Next() 就是递增 index,执行下一个 handler
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

// Abort() 设置 index 为最大值,跳过后续 handler
func (c *Context) Abort() {
c.index = abortIndex // math.MaxInt8 / 2
}
Next vs Abort
  • c.Next():执行后续 handler,执行完后回到当前中间件继续执行后半段
  • c.Abort()跳过后续所有 handler,但当前中间件的后半段仍会执行
  • c.AbortWithStatusJSON():Abort + 写响应,最常用

常用中间件

r := gin.New() // 空 Engine,不含默认中间件

// 全局中间件
r.Use(gin.Logger()) // 请求日志
r.Use(gin.Recovery()) // panic 恢复
r.Use(CORSMiddleware()) // 跨域
r.Use(RateLimiter()) // 限流

// 路由组中间件
api := r.Group("/api", AuthMiddleware())
admin := r.Group("/admin", AuthMiddleware(), AdminOnly())

实战:常用中间件实现

鉴权中间件

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}

claims, err := parseToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}

// 将用户信息存入 Context,后续 Handler 可以取
c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}

CORS 中间件

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

if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

请求耗时 + RequestID

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

net/http 标准中间件模式

// 标准库中间件就是函数嵌套
type Middleware func(http.Handler) http.Handler

func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // 调用下一层
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}

// 使用
mux := http.NewServeMux()
handler := LoggingMiddleware(AuthMiddleware(mux))
http.ListenAndServe(":8080", handler)

常见面试问题

Q1: 中间件中不调用 c.Next() 会怎样?

答案:当前中间件执行完后,Gin 会自动执行下一个 handler(因为 Next 内部是 for 循环递增 index)。不调用 Next() 意味着无法在请求后执行逻辑。如果想阻止后续 handler 执行,必须调用 c.Abort()

Q2: Gin 中间件和 net/http 中间件有什么区别?

答案

维度Ginnet/http
形式gin.HandlerFuncfunc(http.Handler) http.Handler
链接方式r.Use() 注册到数组函数嵌套包装
流程控制c.Next() / c.Abort()调不调 next.ServeHTTP()
数据传递c.Set() / c.Get()context.WithValue()

Q3: 如何控制中间件只对部分路由生效?

答案:使用路由组

// 公开路由,无需鉴权
public := r.Group("/")
public.GET("/health", healthCheck)

// 需要鉴权的路由
auth := r.Group("/api", AuthMiddleware())
auth.GET("/users", listUsers)

// 需要管理员权限
admin := auth.Group("/admin", AdminOnly())
admin.DELETE("/users/:id", deleteUser)

相关链接