Go HTTP拦截本质是链式包装Handler:通过func(http.Handler) http.Handler函数封装原始handler,调用next.ServeHTTP(w, r)执行下游逻辑;需注意body重放、context生命周期及中间件顺序。

Go HTTP 请求拦截的本质是 Wrap Handler
Go 标准库没有“拦截器”概念,所谓拦截,实际是通过 http.Handler 链式包装实现的:把原始 handler 作为参数传给一个新函数,该函数返回一个增强版 handler。所有逻辑(如日志、鉴权、超时)都写在这个包装函数里。
关键点在于:必须调用 next.ServeHTTP(w, r) 才会真正执行下游逻辑;不调用就等于“拦截并终止”。常见错误是忘记这一行,或在错误路径中提前 return 却没写 return 导致后续逻辑仍被执行。
- 包装函数必须满足
func(http.Handler) http.Handler签名才可复用 - 中间件顺序很重要:先注册的外层,后注册的内层(类似洋葱模型)
- 不要在中间件里修改
*http.Request.URL后不调用r = r.Clone(r.Context()),否则可能引发 panic
用 http.HandlerFunc 实现轻量级中间件
如果只是加个日志或 header,没必要定义完整 struct,直接用闭包更简洁。例如记录请求耗时:
func loggingMiddleware(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
这种写法比自定义 struct + ServeHTTP 方法更轻量,也更容易测试——你可以把任意 http.Handler(包括 http.HandlerFunc)传进去。
立即学习“go语言免费学习笔记(深入)”;
- 注意:闭包捕获的变量(如
next)在并发请求下是安全的,Go 的 goroutine 天然隔离 - 若需注入配置(如 logger 实例),用带参数的工厂函数:
func newLoggingMiddleware(logger *log.Logger) func(http.Handler) http.Handler - 避免在闭包里做阻塞操作(如同步写磁盘日志),应改用异步通道或结构化日志库
处理 request.Body 被读取一次的问题
HTTP body 是 io.ReadCloser,标准库只允许读一次。中间件里若调用 io.ReadAll(r.Body)(比如解析 JSON),下游 handler 就会读到空内容。这是最常踩的坑。
正确做法是:读完后用 bytes.NewReader 把数据塞回 r.Body,并重置 r.ContentLength:
body, _ := io.ReadAll(r.Body) r.Body.Close() r.Body = io.NopCloser(bytes.NewReader(body)) r.ContentLength = int64(len(body))
- 务必调用
r.Body.Close(),否则连接可能无法复用(尤其 HTTP/2 场景) - 如果 body 很大(>1MB),别全读进内存,改用
io.TeeReader或临时文件 - 第三方库如
github.com/gorilla/handlers的LoggingHandler默认不读 body,但自定义鉴权中间件常需要解析,这时必须手动处理
Context 传递与超时控制要统一入口
不要在每个中间件里单独调用 context.WithTimeout,容易嵌套混乱。推荐在最外层(如路由注册处)统一加超时,并通过 context.WithValue 注入必要信息(如 traceID):
http.Handle("/api/", timeoutMiddleware(
context.WithValue(
context.Background(),
"traceID", generateTraceID(),
),
http.HandlerFunc(apiHandler),
))
这样下游 handler 可以从 r.Context().Value("traceID") 安全取值,且整个链路共享同一个 deadline。
- 超时中间件必须包裹所有 handler,包括静态文件服务(
http.FileServer) - 使用
context.WithCancel时,记得在 handler 返回前调用 cancel 函数,防止 goroutine 泄漏 - 避免用
context.WithValue传业务结构体,只传简单类型(string/int)或预定义 key,否则类型断言易出错










