中间件签名必须是func(http.Handler) http.Handler,漏掉该契约会导致中间件静默失效;正确结构为接收并返回http.Handler,且内部显式调用next.ServeHTTP(w, r)。

中间件签名必须是 func(http.Handler) http.Handler
漏掉这个契约,中间件就静默失效——编译不报错,但请求根本不会经过它。很多开发者写成 func(http.ResponseWriter, *http.Request) 或混用 gin.HandlerFunc,结果传给 http.Handle() 时被直接忽略,调试时只能翻源码或看 IDE 类型提示才发现问题。
正确结构是:接收一个 http.Handler,返回一个新的 http.Handler,并在内部显式调用 next.ServeHTTP(w, r)。这不是语法糖,而是控制流的唯一入口点。
- 错误典型:
func MyMiddleware(h http.HandlerFunc) http.HandlerFunc—— 只能包装单个 handler,无法嵌套、不可复用 - 带参数的中间件(如超时)必须是工厂函数:
func(timeout time.Duration) func(http.Handler) http.Handler,确保每次生成独立闭包 - 别把
http.HandlerFunc当中间件用;它和http.Handler是不同接口,前者是后者的一种实现
避免隐式分配和 goroutine 阻塞
高并发下,瓶颈常不在业务逻辑,而在 GC 压力和阻塞等待。比如日志拼接用 fmt.Sprintf 会触发字符串分配,defer 在高频中间件里也有微小开销,而没设超时的 Redis 查询可能拖垮整条链。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 日志用
zerolog或log.Printf的预分配字段,避免fmt.Sprintf构造消息 - 鉴权、限流等 IO 操作必须设超时,并用
context.WithTimeout控制生命周期 -
defer只在真需要 recover 的地方用;高频中间件中可提前判断是否需 panic 捕获 - 响应体压缩仅对小数据启用,大文件直接透传,避免内存拷贝和 gzip 缓冲区分配
Context 是唯一安全的数据传递方式
多个中间件之间共享用户 ID、请求 ID、DB transaction 等,不能靠全局变量、包级 map 或闭包捕获变量——这些都会引发并发读写 panic 或数据污染。
必须用 context.WithValue 向下传递:
- 在中间件中:
r = r.WithContext(context.WithValue(r.Context(), key, value)) - 下游通过
r.Context().Value(key)获取,类型安全且 goroutine 隔离 - key 必须是自定义类型(如
type userIDKey struct{}),避免字符串冲突 - 别在中间件里修改原始
http.Request字段(如r.URL.Path),应通过 context 传递衍生信息
插件化思维 ≠ 直接堆 middleware
真正插件系统要解决热加载、生命周期隔离、元信息管理,而 go-grpc-middleware 或 HTTP 中间件链只是静态函数组合——所有拦截器启动时就固定顺序,无法运行时增删,一个 panic 就拖垮整条链。
如果项目要求「运维能独立启停某监控插件」或「上线后动态加载新鉴权策略」,就得越过三道坎:
- 插件必须编译为独立
.so文件,或通过接口注入(如type Plugin interface { Init(cfg Config) error; UnaryInterceptor() grpc.UnaryServerInterceptor }) - 每个插件内部要封装自己的
recovery.UnaryServerInterceptor,不能只在外层套一层 - 用
map[string]grpc.UnaryServerInterceptor管理插件实例,按配置开关控制是否组装进链
最易被忽略的是 panic 隔离——不是加个外层 recover 就完事,而是每个插件得有自己的错误边界,否则一个插件崩溃,整个服务不可用。


















