中间件执行顺序由函数嵌套结构决定,而非注册顺序;如auth(logging(handler))先鉴权后日志,logging(auth(handler))则相反,括号最外层最先处理请求、最后处理响应。

中间件执行顺序由函数嵌套决定,不是配置顺序
Go 的中间件执行顺序完全取决于你如何调用它们,和框架是否“支持链式注册”无关。比如 auth(logging(handler)) 和 logging(auth(handler)) 是两个完全不同行为的组合:前者先鉴权再日志,后者先打日志再鉴权——哪怕都用 chi 或 gorilla/mux 的 .Use() 方法注册,底层仍是按你传入的顺序做嵌套。
常见错误是以为 router.Use(mw1, mw2) 会自动按注册顺序从外到内包裹,其实 chi 是 mw1(mw2(handler)),gorilla/mux 却是 mw2(mw1(handler))(注册顺序即执行顺序)。不查文档直接套用,很容易翻车。
- 手动嵌套
cors(auth(limit(log(handler))))最可靠,一眼看清执行流向 - 用链式工具函数时,必须确认它是正向遍历还是倒序遍历中间件切片
- 所有中间件最终都要落到
http.Handle("/path", finalHandler),而finalHandler必须是http.Handler类型
为什么 panic 恢复中间件必须放在最外层
因为 panic 只有在它发生时的调用栈上才能被捕获。recover() 只对当前 goroutine 中、且在同层 defer 里定义的函数有效。如果你把 recoveryMiddleware 包在 auth 里面,比如 auth(recovery(handler)),那 auth 里 panic 就逃逸出去了,根本进不到 recovery 的 defer 里。
正确做法是让它成为洋葱最外一层皮:recovery(auth(limit(log(handler))))。这样无论哪一层中间件或 handler panic,都会被最外层的 defer func() { if r := recover(); r != nil { ... } }() 拦住。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
recovery中间件内部必须用defer+recover(),且不能提前 return - 恢复后建议写
http.Error(w, "Internal Server Error", http.StatusInternalServerError),避免响应体未写导致客户端 hang 住 - 别在 recovery 里再 panic —— 那就真没人兜底了
限流中间件必须在鉴权之后,否则形同虚设
如果把 rateLimit 放在 auth 外面,攻击者可以用无效 token 不断刷请求,绕过鉴权直接打爆限流器。而真正合法用户反而可能被误限。这不是逻辑 bug,是安全设计缺陷。
典型部署顺序应为:recovery(auth(rateLimit(logging(handler))))。其中 auth 先筛掉非法请求,rateLimit 再对已认证的用户做频控,logging 记录完整路径(含失败请求),handler 处理业务。
- 限流 key 应基于用户 ID 或 IP+User-Agent 组合,而非 raw IP(NAT 场景下不准)
- 不要在
rateLimit里读取 request.Body,否则后续中间件或 handler 会读不到 - 如果用 redis 做计数,记得加 context 超时,避免阻塞整个请求链
漏掉 next.ServeHTTP 是静默失败,调试最难发现的坑
中间件里忘记写 next.ServeHTTP(w, r),或者写了但被 if 分支挡住(比如鉴权失败后没 return),结果就是 HTTP 请求卡住:无响应、无 error log、连接超时后浏览器报 ERR_EMPTY_RESPONSE。你查日志、看 metrics、抓包都看不到任何异常,只觉得“接口突然不响应了”。
最容易出问题的场景是自定义 responseWriter 包装器、超时中间件、或带 early-return 的鉴权逻辑。
- 所有中间件函数体结尾,必须确保
next.ServeHTTP(w, r)被执行(除非你明确要终止流程) - 用
responseWriterWrapper时,WriteHeader只能调一次,重复调用会 panic - 超时中间件里启动 goroutine 执行
next.ServeHTTP后,主 goroutine 必须 select 等待,不能直接 return
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










