中间件剥离是工程实践中将非业务逻辑从http.handlerfunc中抽离并封装为可复用函数的动作,核心目的是解决重复修改、逻辑耦合与维护困难问题。

中间件剥离 不是 Go 语言的语法特性,而是工程实践中一种明确的**职责分离动作**:把原本混在 http.HandlerFunc 里的非业务逻辑(比如日志、耗时统计、鉴权、跨域头、请求 ID 注入等),抽出来封装成独立、可复用、可组合的函数,再通过包装方式“套”在业务 handler 外层。
这个动作的核心目的不是炫技,而是解决实际维护问题——比如你有 20 个接口,每个都手动加了 time.Now() 和 log.Printf(),后来要加监控上报,就得改 20 处;再加一个 trace ID,又得改 20 处。这就是没做剥离的典型代价。
为什么不能直接改 handler 函数体?
因为那会快速导致三个问题:
- 业务逻辑被噪音淹没,读代码时要跳过重复模板
- 新增公共能力(如加
X-Request-ID响应头)必须逐个 handler 手动补,漏一处就埋坑 - 不同环境需要开关某类逻辑(比如只在 prod 记日志),靠 if/else 分支会让 handler 越来越胖
中间件剥离的最小可行实现
本质就是函数套函数。标准库原生支持,不需要框架:
func Logging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("→ %s %s", r.Method, r.URL.Path)
next(w, r)
log.Printf("← %s %s", r.Method, r.URL.Path)
}
}
func AuthRequired(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
// 使用:一层套一层,顺序即执行顺序
http.HandleFunc("/api/user", Logging(AuthRequired(userHandler)))
注意:AuthRequired 在外层,所以先鉴权再打日志;反过来就会出现「未授权请求也被记日志」的问题。
剥离后容易踩的坑
常见错误不是写不出中间件,而是用错上下文或忽略副作用:
-
r.Context()是只读的,想存值必须用r.WithContext()替换整个*http.Request,否则下游拿不到 - 中间件里调用
w.WriteHeader()或w.Write()后,再调next()可能 panic(比如http.ErrBodyWriteAfterClose) - 多个中间件共享变量(如全局计数器)没加锁,高并发下数据错乱
- 忘记处理
next可能 panic 的情况,导致整个请求链路崩溃而无日志
剥离不等于必须用框架
很多人一提中间件就想到 Gin 的 Use() 或 Echo 的 MiddlewareFunc,但标准库 http.Handler 本身已足够支撑剥离。框架只是帮你省掉 Logging(AuthRequired(...)) 这种嵌套写法,底层仍是函数包装。真正关键的是设计边界:哪些该进中间件(所有请求都走)、哪些该进 service 层(仅部分接口需要)、哪些该进 domain 模型(纯业务规则)。剥离动作本身简单,难的是每次加新逻辑时,能条件反射地问一句:“这东西是不是所有接口都需要?”











