中间件原理
问题
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 中间件有什么区别?
答案:
| 维度 | Gin | net/http |
|---|---|---|
| 形式 | gin.HandlerFunc | func(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)