2. Go HTTP中间件基础:net/http Handler接口、中间件模式、洋葱模型、链式调用
2.1 net/http Handler 接口:一切中间件的起点
Go 标准库 net/http 定义了 HTTP 处理的核心抽象——Handler 接口。任何实现了该接口的类型都可以处理 HTTP 请求,这也是构建中间件的基石。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler 接口只包含一个方法 ServeHTTP,接收 ResponseWriter 用于写入响应,*Request 包含请求的所有信息。中间件本质上是一个接收 Handler 并返回新 Handler 的函数,从而实现对请求/响应流程的拦截与增强。
核心概念: 中间件 = func(next Handler) Handler。这种函数签名模式是 Go HTTP 中间件生态的通用契约。
2.2 中间件模式:函数式组合
中间件模式的核心思想是通过函数式组合,将多个处理层串联起来。每个中间件只关注自己的职责,例如日志记录、鉴权、超时控制等,然后调用 next.ServeHTTP 将控制权传递给下一个处理器。
典型的中间件函数签名如下:
type Middleware func(next http.Handler) http.Handler
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s completed in %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(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 ...
next.ServeHTTP(w, r)
})
}
| 中间件 | 职责 | 执行时机 |
|---|---|---|
| Logger | 记录请求方法、路径、耗时 | 请求前 + 请求后 |
| Auth | 验证 Authorization 头 | 请求前(可中断) |
| Recovery | 捕获 panic,返回 500 | 包裹整个调用链 |
| Timeout | 设置请求超时上下文 | 请求前 |
2.3 洋葱模型:请求与响应的双向穿透
洋葱模型(Onion Model)形象地描述了中间件链的执行顺序。请求从最外层中间件进入,逐层向内穿透,到达核心业务处理器后,响应再逐层向外返回。每一层中间件都可以在请求进入时执行前置逻辑,在响应返回时执行后置逻辑。
实践要点: 洋葱模型要求中间件必须调用
next.ServeHTTP,否则链会中断。前置逻辑在调用前执行,后置逻辑在调用后执行。利用 defer 可以优雅地实现后置逻辑,即使后续处理器发生 panic 也能执行。
2.4 链式调用:构建中间件管道
链式调用(Chaining)是将多个中间件组合成一个完整处理管道的编程模式。Go 中常见的实现方式有两种:嵌套调用和管道函数。
2.4.1 嵌套调用
handler := Logger(Auth(Timeout(http.HandlerFunc(finalHandler))))
这种写法直观但可读性差,中间件数量增多时难以维护。
2.4.2 管道函数
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// 使用
handler := Chain(
http.HandlerFunc(finalHandler),
Logger,
Auth,
Timeout,
)
管道函数从最后一个中间件开始反向包装,确保执行顺序与传入顺序一致:先传入的中间件(Logger)在最外层,后传入的(Timeout)靠近核心。
| 方式 | 可读性 | 扩展性 | 推荐场景 |
|---|---|---|---|
| 嵌套调用 | 低 | 低 | 中间件 ≤ 3 个 |
| 管道函数 | 高 | 高 | 中间件 ≥ 3 个 |
| 第三方库(如 negroni、alice) | 最高 | 高 | 团队协作项目 |
2.5 完整示例:构建一个迷你中间件链
package main
import (
"log"
"net/http"
"time"
)
type Middleware func(next http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[REQ] %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[RES] %s %s took %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Recovery(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)
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, Middleware!"))
}
func main() {
finalHandler := http.HandlerFunc(helloHandler)
handler := Chain(finalHandler, Logger, Recovery)
http.Handle("/", handler)
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
常见陷阱: 中间件中如果调用了
http.Error 或 w.WriteHeader 后仍然调用 next.ServeHTTP,会导致重复写入响应头(panic)。正确的做法是在中断请求时直接 return,不再调用 next。
2.6 中间件执行顺序可视化
下面用 SVG 展示一个包含 4 个中间件的链式调用执行流程,清晰呈现请求与响应的完整路径。
2.7 本章小结
- Handler 接口是 Go HTTP 中间件的基石,中间件本质是
func(next Handler) Handler。 - 洋葱模型描述了请求从外到内、响应从内到外的双向穿透过程,每层可执行前置和后置逻辑。
- 链式调用通过管道函数将多个中间件组合成可维护的处理链,推荐使用反向遍历实现。
- 中间件必须谨慎处理中断场景,避免重复写入响应头;利用
defer实现可靠的后置清理逻辑。