中间件模式:构建灵活的 Web 应用
在 Web 开发中,有很多横切关注点(cross-cutting concerns)需要在每个请求中处理:日志记录、身份验证、权限检查、请求限流、CORS 处理等等。如果把这些逻辑都写在每个 handler 里,代码会变得冗余且难以维护。
中间件(Middleware)模式就是为了解决这个问题而生的。它允许我们将这些通用逻辑抽取出来,形成可复用的组件,像管道一样串联起来处理请求。
什么是中间件?
中间件本质上是一个函数,它:
- 接收一个
http.Handler作为输入 - 返回一个新的
http.Handler - 在调用下一个 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 ""
}
总结
中间件的洋葱模型与执行顺序
理解中间件的执行顺序是正确使用中间件的关键。中间件的洋葱模型形象地描述了请求和响应的流向:
请求进入
|
[CORSMiddleware] <--- 最外层
|
[LoggingMiddleware]
|
[AuthMiddleware]
|
[RecoveryMiddleware]
|
[Handler] <--- 核心处理
|
[RecoveryMiddleware] <--- 返回开始
|
[AuthMiddleware]
|
[LoggingMiddleware]
|
[CORSMiddleware]
|
响应离开
这是一个后进先出(LIFO)的执行模式。请求阶段按注册顺序执行,响应阶段按相反顺序执行。这个特性非常重要,比如日志中间件需要在响应离开之前记录耗时,所以它必须包裹在最内层之上。
中间件执行顺序的代码演示
package main
import (
"fmt"
"net/http"
)
type Middleware func(http.Handler) http.Handler
func MiddlewareA(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("A - 请求进入")
next.ServeHTTP(w, r)
fmt.Println("A - 响应离开")
})
}
func MiddlewareB(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("B - 请求进入")
next.ServeHTTP(w, r)
fmt.Println("B - 响应离开")
})
}
func MiddlewareC(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("C - 请求进入")
next.ServeHTTP(w, r)
fmt.Println("C - 响应离开")
})
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handler - 核心处理")
w.Write([]byte("OK"))
})
// 执行顺序: A -> B -> C -> Handler -> C -> B -> A
chain := MiddlewareA(MiddlewareB(MiddlewareC(handler)))
http.ListenAndServe(":8080", chain)
}
输出:
A - 请求进入
B - 请求进入
C - 请求进入
Handler - 核心处理
C - 响应离开
B - 响应离开
A - 响应离开
Gin 框架的中间件集成
Gin 是 Go 最受欢迎的 Web 框架之一,它的中间件实现非常直观。Gin 使用 gin.HandlerFunc 类型,本质上是 func(*Context)。
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 执行后续中间件和 handler
fmt.Printf("[%s] %s %s %v\n",
c.Request.Method,
c.Request.URL.Path,
c.ClientIP(),
time.Since(start),
)
}
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort() // 终止后续处理
return
}
c.Set("userID", "12345")
c.Next()
}
}
func main() {
r := gin.New()
// 全局中间件
r.Use(LoggerMiddleware())
r.Use(gin.Recovery())
// 公开路由
r.GET("/public", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "公开内容"})
})
// 需要认证的路由组
authGroup := r.Group("/api")
authGroup.Use(AuthMiddleware())
{
authGroup.GET("/profile", func(c *gin.Context) {
userID := c.MustGet("userID").(string)
c.JSON(200, gin.H{"userID": userID})
})
}
// 带超时中间件的特定路由
r.GET("/slow", TimeoutMiddleware(2*time.Second), func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.JSON(200, gin.H{"message": "完成"})
})
r.Run(":8080")
}
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
done := make(chan struct{})
go func() {
c.Next()
close(done)
}()
select {
case <-done:
return
case <-time.After(timeout):
c.AbortWithStatusJSON(504, gin.H{"error": "请求超时"})
}
}
}
Echo 框架的中间件对比
Echo 框架的中间件设计更加函数式,与标准库的 http.Handler 模式不同,Echo 使用 echo.HandlerFunc 和 echo.MiddlewareFunc。
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// 内置中间件
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodPost},
}))
// 自定义中间件
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
requestID := c.Request().Header.Get("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("%d", time.Now().UnixNano())
}
c.Set("requestID", requestID)
c.Response().Header().Set("X-Request-ID", requestID)
return next(c)
}
})
// 路由级别中间件
e.GET("/health", func(c echo.Context) error {
return c.JSON(200, map[string]string{"status": "ok"})
})
e.GET("/users", AuthMiddleware, func(c echo.Context) error {
return c.JSON(200, []string{"user1", "user2"})
})
e.Start(":8080")
}
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 认证逻辑
return next(c)
}
}
中间件性能对比
不同中间件的实现方式会带来不同的性能表现。以下是几种常见模式的性能对比:
| 实现方式 | 每次请求开销 | 适用场景 |
|---|---|---|
| 标准库 http.Handler | ~50 ns | 通用,兼容性最好 |
| gin.HandlerFunc | ~30 ns | Gin 项目,性能最优 |
| echo.MiddlewareFunc | ~40 ns | Echo 项目,函数式风格 |
| 闭包中间件 | ~60 ns | 需要外部状态 |
package main
import (
"fmt"
"net/http"
"testing"
)
func BenchmarkMiddlewareChain(b *testing.B) {
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
// 构建 5 层中间件
handler := finalHandler
for i := 0; i < 5; i++ {
next := handler
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := &mockResponseWriter{}
handler.ServeHTTP(w, r)
}
}
type mockResponseWriter struct{}
func (m *mockResponseWriter) Header() http.Header { return http.Header{} }
func (m *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
func (m *mockResponseWriter) WriteHeader(int) {}
var r, _ = http.NewRequest("GET", "/test", nil)
func main() {
fmt.Println("中间件链的性能开销非常低,通常可以忽略不计")
}
高级中间件模式
条件中间件
有时候需要根据请求的条件决定是否应用某个中间件:
func ConditionalMiddleware(condition func(*http.Request) bool, middleware Middleware) Middleware {
return func(next http.Handler) http.Handler {
wrapped := middleware(next)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if condition(r) {
wrapped.ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}
}
// 只对 API 路由应用认证
condition := func(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, "/api/")
}
handler := ConditionalMiddleware(condition, AuthMiddleware)(mux)
中间件参数化工厂
使用工厂模式生成带配置参数的中间件:
type RateLimitConfig struct {
RequestsPerSecond float64
BurstSize int
}
func RateLimitMiddleware(config RateLimitConfig) Middleware {
limiter := rate.NewLimiter(rate.Limit(config.RequestsPerSecond), config.BurstSize)
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)
})
}
}
常见错误与 FAQ
Q: 中间件的 panic 为什么会 vs recovered 的方式不同?
A: Recovery 中间件必须包裹在所有可能 panic 的中间件外层。在标准库的 Handler 模型中,这意味着 recovery 应该在链式组合的最后注册。而在 Gin 中,gin.Recovery() 会自动在最外层生效。
Q: context 值传递的最佳实践是什么?
A: 永远使用自定义类型作为 context key,避免命名冲突:
type contextKey string
const (
UserIDKey contextKey = "userID"
RequestIDKey contextKey = "requestID"
)
// 不要使用字符串作为 key
ctx := context.WithValue(r.Context(), "userID", id) // 不推荐
ctx := context.WithValue(r.Context(), UserIDKey, id) // 推荐
Q: 如何在中间件中正确修改响应内容?
A: 标准库的 http.ResponseWriter 是写一次(write-once)的。如果需要查看或修改响应,需要包装它:
type responseRecorder struct {
http.ResponseWriter
statusCode int
body []byte
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.statusCode = code
rr.ResponseWriter.WriteHeader(code)
}
func (rr *responseRecorder) Write(b []byte) (int, error) {
rr.body = append(rr.body, b...)
return rr.ResponseWriter.Write(b)
}
Q: 跨框架的中间件是否可以通用?
A: 不可以直接通用,因为各框架的 Handler 接口不同。但可以通过适配器模式转换:
func GinToStdMiddleware(ginMW gin.HandlerFunc) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 适配逻辑
next.ServeHTTP(w, r)
})
}
}
Q5: 中间件的数量是否会影响请求性能?
A: 标准库中间件每次只是一个函数调用,开销极小。即使叠加 10 层中间件,单次请求额外开销也不到 1 微秒。真正影响性能的是中间件内执行的操作,如数据库查询等,而不是中间件机制本身。
Q6: 中间件和 AOP 编程有什么区别?
A: 中间件是 AOP 思想在 Web 服务器中的具体体现。AOP 是编程范式,中间件是 Go 中利用接口组合和闭包实现该范式的技术手段。Go 没有注解系统,不能像 Java 那样直接声明切面,而是通过链式组合实现。
Q7: 如何在中间件中提前终止请求链?
A: 中间件中如果不调用 next.ServeHTTP,则请求链被终止。例如认证失败时直接返回 401 错误而不调用后续中间件和业务 handler。但要注意只有外层中间件可以可靠地终止链。
Q8: 中间件中的 panic 恢复为什么不自动处理 HTTP 错误?
A: Recover 只负责调用 panic 的恢复以防止进程崩溃,具体的 HTTP 响应需要手动构造。在标准库模式中可以写入 500 状态码和错误提示。Gin 的 Recovery 中间件则会输出堆栈跟踪和错误页面。
中间件设计最佳实践总结
- 单一职责:每个中间件只做一件事,复杂逻辑拆分成多个小的中间件
- 顺序关系:了解洋葱模型,正确安排中间件顺序
- 错误处理:使用 panic recovery 保护服务稳定性
- Context 传递:使用类型安全的 context key 而非字符串
- 性能意识:中间件虽然开销小,但不要无意义地增加层数
- 可测试性:每个中间件应能独立测试
延伸阅读
- Writing HTTP Middleware in Go — Alex Edwards 的经典文章
- Gin Middleware 文档
- Echo Middleware 文档
- Go http.Handler 接口详解
总结
中间件模式是 Go Web 开发的核心模式,它让我们能够:
- 分离关注点:将通用逻辑从业务逻辑中分离
- 代码复用:一次编写,多处使用
- 灵活组合:按需添加和配置中间件
- 易于测试:每个中间件可以独立测试
记住几个最佳实践:
- 中间件应该只做一件事
- 注意中间件的执行顺序
- 使用 context 传递请求级别的数据
- 错误处理要优雅,不要让 panic 传播
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。