中间件模式:构建灵活的 Web 应用

深入理解 Go Web 开发中的中间件模式,学习如何构建可复用的请求处理链

中间件模式:构建灵活的 Web 应用

在 Web 开发中,有很多横切关注点(cross-cutting concerns)需要在每个请求中处理:日志记录、身份验证、权限检查、请求限流、CORS 处理等等。如果把这些逻辑都写在每个 handler 里,代码会变得冗余且难以维护。

中间件(Middleware)模式就是为了解决这个问题而生的。它允许我们将这些通用逻辑抽取出来,形成可复用的组件,像管道一样串联起来处理请求。

什么是中间件?

中间件本质上是一个函数,它:

  1. 接收一个 http.Handler 作为输入
  2. 返回一个新的 http.Handler
  3. 在调用下一个 handler 之前或之后执行一些逻辑
type Middleware func(http.Handler) http.Handler

基础中间件实现

让我们从最简单的日志中间件开始:

package main

import (
    "log"
    "net/http"
    "time"
)

// LoggingMiddleware 记录每个请求的处理时间
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // 调用下一个 handler
        next.ServeHTTP(w, r)
        
        // 记录日志
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })
    
    // 使用中间件包装 mux
    handler := LoggingMiddleware(mux)
    http.ListenAndServe(":8080", handler)
}

这个中间件在请求处理前记录开始时间,在请求处理后计算耗时并打印日志。

链式中间件

实际项目中,我们通常需要多个中间件。手动嵌套会变得很丑陋:

// 丑陋的方式
handler := Middleware1(Middleware2(Middleware3(mux)))

让我们写一个辅助函数来简化:

// Chain 将多个中间件串联起来
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

// 使用方式
handler := Chain(mux,
    LoggingMiddleware,
    RecoveryMiddleware,
    CORSMiddleware,
)

注意中间件的执行顺序:先注册的中间件在最外层,后注册的在最内层。

常用中间件实现

1. 恢复中间件(Panic Recovery)

防止 panic 导致整个服务崩溃:

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

2. CORS 中间件

处理跨域请求:

func CORSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        // 处理预检请求
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

3. 请求 ID 中间件

为每个请求生成唯一 ID,方便追踪:

import "github.com/google/uuid"

func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = uuid.New().String()
        }
        
        // 将 requestID 添加到响应头
        w.Header().Set("X-Request-ID", requestID)
        
        // 将 requestID 添加到 context
        ctx := context.WithValue(r.Context(), "requestID", requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

4. 身份验证中间件

检查 JWT Token:

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        // 验证 token
        claims, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        // 将用户信息添加到 context
        ctx := context.WithValue(r.Context(), "user", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

5. 限流中间件

控制请求频率:

import "golang.org/x/time/rate"

func RateLimitMiddleware(limit rate.Limit, burst int) Middleware {
    limiter := rate.NewLimiter(limit, burst)
    
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 使用:每秒最多 10 个请求,突发最多 20 个
handler := Chain(mux, RateLimitMiddleware(10, 20))

带参数的中间件

有时候中间件需要配置参数,可以使用闭包:

// TimeoutMiddleware 设置请求超时
func TimeoutMiddleware(timeout time.Duration) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), timeout)
            defer cancel()
            
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// 使用
handler := Chain(mux, TimeoutMiddleware(30*time.Second))

路由级别的中间件

有时候我们只想在某些路由上应用中间件:

func main() {
    mux := http.NewServeMux()
    
    // 公开路由
    mux.HandleFunc("/login", loginHandler)
    mux.HandleFunc("/register", registerHandler)
    
    // 受保护的路由
    protectedMux := http.NewServeMux()
    protectedMux.HandleFunc("/profile", profileHandler)
    protectedMux.HandleFunc("/settings", settingsHandler)
    
    // 只给受保护的路由添加认证中间件
    protectedHandler := AuthMiddleware(protectedMux)
    
    // 将所有路由组合到主 mux
    mainMux := http.NewServeMux()
    mainMux.Handle("/", mux)
    mainMux.Handle("/api/", protectedHandler)
    
    // 全局中间件
    handler := Chain(mainMux,
        LoggingMiddleware,
        RecoveryMiddleware,
    )
    
    http.ListenAndServe(":8080", handler)
}

实战:构建完整的中间件栈

package main

import (
    "context"
    "log"
    "net/http"
    "time"
)

// ContextKey 自定义 context key 类型
type ContextKey string

const (
    RequestIDKey ContextKey = "requestID"
    UserIDKey    ContextKey = "userID"
)

// Middleware 中间件类型定义
type Middleware func(http.Handler) http.Handler

// Chain 串联中间件
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

// LoggingMiddleware 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        requestID, _ := r.Context().Value(RequestIDKey).(string)
        
        next.ServeHTTP(w, r)
        
        log.Printf("[%s] %s %s %v",
            requestID,
            r.Method,
            r.URL.Path,
            time.Since(start),
        )
    })
}

// RecoveryMiddleware 恢复中间件
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// RequestIDMiddleware 请求 ID 中间件
func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateID()
        }
        
        w.Header().Set("X-Request-ID", requestID)
        ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// AuthMiddleware 认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        // 简化验证
        userID := validateToken(token)
        if userID == "" {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        ctx := context.WithValue(r.Context(), UserIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// CORSMiddleware CORS 中间件
func CORSMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    
    // 公开 API
    mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })
    
    // 受保护的 API
    apiMux := http.NewServeMux()
    apiMux.HandleFunc("/api/users", usersHandler)
    apiMux.HandleFunc("/api/profile", profileHandler)
    
    // 为受保护的路由添加认证中间件
    protectedAPI := Chain(apiMux, AuthMiddleware)
    
    // 组合所有路由
    mainMux := http.NewServeMux()
    mainMux.Handle("/", mux)
    mainMux.Handle("/api/", protectedAPI)
    
    // 应用全局中间件
    handler := Chain(mainMux,
        RecoveryMiddleware,
        RequestIDMiddleware,
        LoggingMiddleware,
        CORSMiddleware,
    )
    
    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", handler)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value(UserIDKey).(string)
    w.Write([]byte("Users for: " + userID))
}

func profileHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value(UserIDKey).(string)
    w.Write([]byte("Profile for: " + userID))
}

func generateID() string {
    return time.Now().Format("20060102150405")
}

func validateToken(token string) string {
    // 简化实现
    if token == "valid-token" {
        return "user123"
    }
    return ""
}

总结

中间件模式是 Go Web 开发的核心模式,它让我们能够:

  1. 分离关注点:将通用逻辑从业务逻辑中分离
  2. 代码复用:一次编写,多处使用
  3. 灵活组合:按需添加和配置中间件
  4. 易于测试:每个中间件可以独立测试

记住几个最佳实践:

  • 中间件应该只做一件事
  • 注意中间件的执行顺序
  • 使用 context 传递请求级别的数据
  • 错误处理要优雅,不要让 panic 传播

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页