HTTP 中间件是 Web 服务里非常常见的设计。日志、鉴权、跨域、压缩、panic 恢复、限流、请求追踪都可以通过中间件实现。理解中间件链之后,即使不使用大型 Web 框架,也能基于标准库写出清晰的服务结构。
本文使用 Go 标准库的 net/http 实现一个简单中间件链,并说明执行顺序、错误处理和扩展方式。
中间件的本质
在标准库里,一个 HTTP 处理器只要实现 http.Handler 接口即可:
1 2 3
| type Handler interface { ServeHTTP(ResponseWriter, *Request) }
|
中间件可以理解为一个函数:接收一个 http.Handler,返回一个新的 http.Handler。
1
| type Middleware func(http.Handler) http.Handler
|
这样就可以在真正的业务 handler 前后增加逻辑。
编写日志中间件
先实现一个简单的访问日志中间件:
1 2 3 4 5 6 7 8 9
| func Logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s cost=%s", r.Method, r.URL.Path, time.Since(start)) }) }
|
这里的关键是 next.ServeHTTP(w, r)。它代表调用链继续向后执行。调用前可以记录开始时间,调用后可以记录耗时。
panic 恢复中间件
服务端代码难免存在未预料的 panic。中间件可以统一 recover,避免整个进程退出:
1 2 3 4 5 6 7 8 9 10 11 12
| func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if v := recover(); v != nil { log.Printf("panic path=%s err=%v", r.URL.Path, v) http.Error(w, "internal server error", http.StatusInternalServerError) } }()
next.ServeHTTP(w, r) }) }
|
这个中间件应该放在比较外层,这样后续中间件和业务 handler 发生 panic 时都能被捕获。
鉴权中间件
再写一个简单的 token 鉴权中间件:
1 2 3 4 5 6 7 8 9 10 11
| func Auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "Bearer demo-token" { http.Error(w, "unauthorized", http.StatusUnauthorized) return }
next.ServeHTTP(w, r) }) }
|
如果鉴权失败,不调用 next.ServeHTTP,请求就在当前中间件终止。这个特性适合鉴权、限流、参数校验等前置逻辑。
构建中间件链
可以写一个 Chain 函数把多个中间件组合起来:
1 2 3 4 5 6
| func Chain(h http.Handler, middlewares ...Middleware) http.Handler { for i := len(middlewares) - 1; i >= 0; i-- { h = middlewares[i](h) } return h }
|
注意这里从后往前包裹。假设调用:
1
| handler := Chain(finalHandler, Recover, Logger, Auth)
|
实际执行顺序是:
1
| Recover -> Logger -> Auth -> finalHandler -> Logger -> Recover
|
外层中间件先进入,内层中间件先返回。
完整示例
下面是一个可以直接运行的小服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| package main
import ( "log" "net/http" "time" )
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, middlewares ...Middleware) http.Handler { for i := len(middlewares) - 1; i >= 0; i-- { h = middlewares[i](h) } return h }
func main() { mux := http.NewServeMux()
mux.Handle("/hello", Chain( http.HandlerFunc(hello), Recover, Logger, Auth, ))
log.Println("server listen on :8080") log.Fatal(http.ListenAndServe(":8080", mux)) }
func hello(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello")) }
func Logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s cost=%s", r.Method, r.URL.Path, time.Since(start)) }) }
func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if v := recover(); v != nil { log.Printf("panic: %v", v) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) }
func Auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer demo-token" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) }
|
测试请求:
1 2
| curl -i http://localhost:8080/hello curl -i -H 'Authorization: Bearer demo-token' http://localhost:8080/hello
|
第一个请求会返回 401,第二个请求返回 hello。
记录状态码
上面的日志中间件没有记录响应状态码,因为标准库的 ResponseWriter 不会直接暴露状态。可以包装它:
1 2 3 4 5 6 7 8 9
| type statusWriter struct { http.ResponseWriter status int }
func (w *statusWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) }
|
在日志中间件中使用:
1 2 3
| sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r) log.Printf("status=%d", sw.status)
|
这样就能记录每个请求的最终状态码。
小结
Go 的 HTTP 中间件并不依赖框架。只要理解 func(http.Handler) http.Handler 这个签名,就可以把通用逻辑从业务 handler 中拆出来。实践中建议把 recover、日志、trace id 放在外层,鉴权、限流、参数校验放在业务入口前,保持链路清晰。