func(http.Handler) http.Handler是唯一正确签名,其他写法会导致编译失败或运行时panic;必须显式调用next.ServeHTTP、提前终止需return、r.Body须缓存、跨中间件传值仅能通过context.Context。

func(http.Handler) http.Handler 是唯一能串起来的签名,其他写法要么编译失败,要么嵌套时 panic。
中间件函数签名必须严格是 func(http.Handler) http.Handler
Go 没有装饰器语法,所谓“中间件”就是接收一个 http.Handler、返回另一个 http.Handler 的函数。写成 func(http.HandlerFunc) http.HandlerFunc 看似能编译,但一旦下游是 chi.Mux 或自定义 struct 实现的 http.Handler,类型就不匹配了。
-
func auth(next http.Handler) { }—— 没返回值,http.ListenAndServe启动直接 panic -
func auth(next http.Handler) func(http.ResponseWriter, *http.Request) { }—— 返回类型不是http.Handler,无法接入标准 handler 链 - 正确写法必须用
http.HandlerFunc包裹闭包,再转为http.Handler:func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) }) }
提前终止请求时必须 return,且不能调用 next.ServeHTTP()
鉴权失败、参数校验不通过、限流触发等场景下,写了 http.Error(w, "Unauthorized", http.StatusUnauthorized) 就得立刻 return。否则 next.ServeHTTP(w, r) 仍会执行,可能造成重复响应或敏感信息泄露。
- 典型错误:
if token == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) }后没return,下面的next.ServeHTTP还会跑 - 更隐蔽的问题:在
next.ServeHTTP()之后再调http.Error(),此时 header 可能已 flush,触发http: multiple response.WriteHeader callspanic - 建议封装工具函数:
func writeError(w http.ResponseWriter, status int, msg string) { if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") } http.Error(w, msg, status) }
读取 r.Body 前必须缓存,否则下游收不到数据
r.Body 是 io.ReadCloser,只能读一次。日志中间件里用 io.ReadAll(r.Body) 打印 body 后,下游 handler 再读就是空字节——这不是 bug,是设计使然。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 正确做法:用
io.ReadCloser包装原始 body,并重置r.Body:body, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewBuffer(body)) // 后续可再次读取 body
- 如果只记录日志不改逻辑,建议只读关键字段(如
r.URL.Query().Get("id")),避免全文读取 - 别在中间件里直接
json.NewDecoder(r.Body).Decode(&v),除非你确定下游不再需要原始 body
跨中间件传值只能靠 context.Context,别用闭包或全局变量
并发环境下,闭包捕获变量或包级全局变量会导致数据竞争。唯一安全方式是通过 r.Context().WithValue(key, value) 写入,再用 r.WithContext(ctx) 传下去。
立即学习“go语言免费学习笔记(深入)”;
-
key必须是自定义类型(比如type userIDKey struct{}),避免字符串 key 冲突 - 读值时务必判空 + 类型断言:
if id, ok := r.Context().Value(userIDKey{}).(string); ok { ... } - 别用
context.Background()替代 request context——它没有 cancel 和 timeout 信号 - 中间件顺序影响传值时机:越外层的中间件越早拿到请求,也越晚拿到响应,
context值必须在调用next.ServeHTTP前写入
next.ServeHTTP 的调用位置和 context 的生命周期管理——前者决定洋葱模型的执行流,后者决定数据是否真的能被下游拿到。这两处出错,调试时往往看不出明显报错,只表现为逻辑静默失效。

















