结论:应按场景选择封装粒度——http请求包装器专注重试、超时、header透传,handler中间件须符合func(http.handler) http.handler签名,限流需复用rate.limiter,响应结构化应由handler负责而非中间件篡改响应体。

直接说结论:别从零写“通用包装器”,先按场景选封装粒度——是包装单个 HTTP 请求(如 client.Do),还是包装 handler(如 ServeHTTP)?两者目的、接口、错误处理逻辑完全不同,混用必踩坑。
http.Client 请求包装器:重点在重试、超时、Header 透传
这类包装器本质是 func(*http.Request) (*http.Response, error) 的增强版,不是中间件。常见需求是自动重试失败请求、统一加认证头、保留重定向时的自定义 Header。
- 别直接改
http.DefaultClient:它被全局共享,修改后会影响所有未显式指定 client 的调用 - 重试逻辑必须检查错误类型:
url.Error可重试,net.OpError(如连接拒绝)可重试,但json.SyntaxError这类响应解析错误不能重试 - 重定向丢失 Header 是默认行为:必须自定义
CheckRedirect,且要显式把原始请求的Authorization、X-Request-ID等复制过去 - 超时必须分层设:client-level 的
Timeout会覆盖 transport 层设置;更稳妥的是只设Transport的IdleConnTimeout和ResponseHeaderTimeout,再用 context.WithTimeout 包裹每次Do()
http.Handler 中间件包装器:核心是类型签名和执行顺序
真正叫“包装器”的,是那种接收 http.Handler、返回 http.Handler 的函数。签名必须是 func(http.Handler) http.Handler,否则无法链式嵌套。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 错误现象:
cannot use myMiddleware (type func(http.ResponseWriter, *http.Request)) as type http.Handler—— 这说明你写的不是中间件,只是个裸 handler - 别在中间件里提前写响应后还调
next.ServeHTTP():Go 的http.ResponseWriter不允许多次调用WriteHeader(),会 panic - 顺序决定行为:日志中间件放最外层能记录所有请求;认证中间件放日志之后、业务之前,才能既打日志又拦非法请求
- 调试时堆栈变深:用变量逐步包装比匿名函数嵌套更易定位问题,比如
h := authMiddleware(loggingMiddleware(homeHandler))比authMiddleware(loggingMiddleware(http.HandlerFunc(...)))清晰
限流/熔断类包装器:rate.Limiter 必须复用,不能每次新建
这类包装器常被误认为“装饰器”,实际就是闭包捕获一个 *rate.Limiter 实例,再套一层函数调用逻辑。
- 常见错误:在包装函数内部 new 一个
rate.NewLimiter()—— 每次调用都新建限流器,完全失效 - 必须用
context.Context传入limiter.Wait():硬编码context.Background()会导致上游 timeout 无法中断等待 - 限流失败要区分返回:
rate.ErrLimited是限流错误,业务函数返回的 error 是业务错误,下游需不同策略处理 - 如果包装的是 handler,注意
limiter.Wait()在ServeHTTP入口就该调用,别等进业务逻辑才限流,否则并发可能超阈值
结构化响应包装器:别在中间件里序列化 data 字段
统一响应格式(如 {code:0, msg:"ok", data:{...}})看似简单,但最容易在中间件里做错:试图用包装 http.ResponseWriter 拦截并修改响应体。
- 原生
net/http不提供读取已写响应体的接口,强行缓存会吃内存、拖慢性能 - 正确做法是业务 handler 自己构造
Response结构体,然后json.NewEncoder(w).Encode(resp)—— 把序列化责任留在 handler 内部 - 中间件只负责加公共字段(如 trace_id、server_time)、统一定制 header(如
X-Response-Time)、记录状态码,不碰data - 如果真要动态注入字段(如用户 ID),应在 request.Context 里塞值,handler 从 ctx 取,而不是中间件去改 JSON 字节流
最易被忽略的一点:所有包装器都依赖底层接口契约。http.Handler 要求实现 ServeHTTP(http.ResponseWriter, *http.Request);http.Client 的包装器必须兼容 Do() 的输入输出;限流包装器必须接受目标函数的签名并原样返回。契约不对,再漂亮的封装也是废代码。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










