Go HTTP 中间件基础:标准库与洋葱模型

在构建分布式系统时,HTTP 中间件是链路性能剖析的基石。本章将深入 Go 标准库 net/http 的核心接口,解析中间件的洋葱模型,并手写第一个日志中间件。掌握这些基础后,你就能在后续章节中无缝接入链路追踪与性能指标采集。

1. 标准库核心:Handler 与 HandlerFunc

Go 的 HTTP 处理模型围绕两个核心类型构建:

类型 定义 作用
http.Handler interface { ServeHTTP(ResponseWriter, *Request) } 所有 HTTP 处理器的通用接口
http.HandlerFunc func(ResponseWriter, *Request) 将普通函数适配为 Handler 的适配器类型

关键理解:HandlerFunc 实现了 Handler 接口,因此任何签名匹配的函数都可以直接作为处理器使用。这种设计让中间件可以无缝包装任意处理器。

💡 核心洞察:ServeHTTP 方法是整个 Go HTTP 中间件体系的入口点。中间件本质上是一个接收 Handler 并返回新 Handler 的函数。

2. 中间件模式:洋葱模型

Go HTTP 中间件遵循经典的洋葱模型(Onion Model)。请求从外层中间件依次向内传递,经过核心处理器后,响应再反向穿过各层中间件。这种结构天然适合日志记录、性能计时、错误恢复等横切关注点。

Go HTTP 中间件洋葱模型 洋葱模型:请求与响应穿透中间件 中间件 1(日志) 中间件 2(性能计时) 中间件 3(链路追踪) 核心 Handler 业务逻辑 请求 响应 📌 请求从外向内逐层传递(橙→紫→青→蓝),响应反向穿出。 每层中间件可同时处理请求前逻辑和响应后逻辑。
🎯 实践要点:洋葱模型要求中间件必须调用 next.ServeHTTP(w, r) 将控制权交给下一层。如果忘记调用,请求链会中断,后续中间件和核心处理器都不会执行。

3. 编写第一个日志中间件

日志中间件是性能剖析的基础设施。它记录每个请求的方法、路径、状态码和耗时,为后续的链路追踪提供原始数据。

3.1 基础实现

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()

        // 包装 ResponseWriter 以捕获状态码
        lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        // 调用下一个处理器
        next.ServeHTTP(lrw, r)

        // 记录日志
        duration := time.Since(start)
        log.Printf("[%s] %s %s %d %v",
            r.Method,
            r.URL.Path,
            r.RemoteAddr,
            lrw.statusCode,
            duration,
        )
    })
}

// loggingResponseWriter 包装 http.ResponseWriter 以捕获状态码
type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

3.2 使用中间件

func main() {
    // 核心处理器
    helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Hello, Middleware!"))
    })

    // 包裹中间件
    wrappedHandler := LoggingMiddleware(helloHandler)

    http.Handle("/hello", wrappedHandler)
    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

3.3 输出示例

2025/01/15 14:30:22 [GET] /hello 127.0.0.1:54321 200 12.345µs
日志中间件执行时序 日志中间件执行时序 t₀ t₁ 记录开始时间 next.ServeHTTP() 计算耗时 关键步骤拆解: 1. 请求到达中间件,记录 t₀(time.Now()) 2. 调用 next.ServeHTTP(),控制权交给下一层或核心处理器 3. 响应返回后,计算耗时 = time.Since(start) 4. 输出结构化日志:方法、路径、状态码、耗时 5. 使用 loggingResponseWriter 包装器捕获状态码
⚠️ 常见陷阱:直接使用 http.ResponseWriter 无法获取响应状态码。必须通过包装器(如 loggingResponseWriter)拦截 WriteHeader 调用。否则日志中的状态码永远是 200。

4. 中间件链式组合

实际项目中通常需要多个中间件协同工作。Go 标准库没有内置中间件链,但可以通过函数组合轻松实现:

// ChainMiddleware 将多个中间件组合成一个
func ChainMiddleware(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        for i := len(middlewares) - 1; i >= 0; i-- {
            next = middlewares[i](next)
        }
        return next
    }
}

// 使用示例
func main() {
    chain := ChainMiddleware(
        LoggingMiddleware,
        RecoveryMiddleware,   // 恢复中间件
        TracingMiddleware,    // 链路追踪中间件
    )

    http.Handle("/api", chain(http.HandlerFunc(apiHandler)))
    http.ListenAndServe(":8080", nil)
}
中间件 职责 执行时机
日志中间件 记录请求方法、路径、状态码、耗时 请求前 + 响应后
恢复中间件 捕获 panic,返回 500 错误 请求前(defer)
链路追踪中间件 注入/提取 Trace ID,记录 Span 请求前 + 响应后
性能计时中间件 精确测量各阶段耗时 请求前 + 响应后
🔧 工程建议:中间件顺序至关重要。日志中间件通常放在最外层,确保所有请求都被记录;恢复中间件应紧贴核心处理器,防止 panic 逃逸;链路追踪中间件需要尽早注入 Trace ID,因此放在较外层。

5. 本章小结

  • Handler 与 HandlerFunc:Go HTTP 处理器的核心接口与适配器,中间件本质是 Handler → Handler 的函数。
  • 洋葱模型:请求从外向内穿透,响应反向穿出,每层中间件可执行前置和后置逻辑。
  • 日志中间件:通过包装 ResponseWriter 捕获状态码,记录请求耗时,是性能剖析的基础设施。
  • 链式组合:通过函数组合实现中间件链,注意顺序对行为的影响。