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)。请求从外层中间件依次向内传递,经过核心处理器后,响应再反向穿过各层中间件。这种结构天然适合日志记录、性能计时、错误恢复等横切关注点。
🎯 实践要点:洋葱模型要求中间件必须调用
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
⚠️ 常见陷阱:直接使用
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捕获状态码,记录请求耗时,是性能剖析的基础设施。 - 链式组合:通过函数组合实现中间件链,注意顺序对行为的影响。