go中间件函数签名必须为func(http.handler) http.handler,否则链式注册会因类型不匹配崩溃;需用http.handlerfunc包装逻辑,注册时必须使用http.handle而非http.handlefunc。

必须用 http.Handler 签名,别写成 http.HandlerFunc 参数
中间件函数签名错一个字,链式注册就崩。常见错误是写成 func logging(next http.HandlerFunc) http.HandlerFunc,这会导致嵌套时类型不匹配:下游中间件接收 http.Handler,你却传了个 http.HandlerFunc(虽然后者实现了接口,但 Go 不做隐式转换)。正确写法统一用 http.Handler 作入参和返回值:
func logging(next http.Handler) http.Handler- 内部用
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... })包装逻辑 - 注册必须用
http.Handle("/path", logging(handler)),不能用http.HandleFunc
状态码和响应长度得靠包装 http.ResponseWriter 拦截
原生 http.ResponseWriter 不暴露状态码和已写字节数,直接打日志会看到 0 或 -1。必须定义 wrapper 结构体,重写 WriteHeader 和 Write:
-
WriteHeader中存下状态码,并标记wroteHeader = true -
Write中若未写 header,先调rw.ResponseWriter.WriteHeader(http.StatusOK),再记录status = 200 - 响应长度只统计
Write写入的字节数,别碰http.Response—— 它压根没这个字段 - 别在
Write里重复调WriteHeader,HTTP 规范禁止多次调用
请求路径要用 r.RequestURI,别信 r.URL.Path
反向代理、URL 重写后,r.URL.Path 常被改为空或 /,而 r.RequestURI 是服务端收到的原始字符串,没被标准库解析篡改过,适合日志归档。但要注意:
-
r.RequestURI带查询参数,敏感字段(如token=xxx)得手动脱敏 - 别用
r.URL.String(),它会补?、漏 fragment,还可能触发额外解析开销 - 如果用了
gorilla/mux,路由名可用mux.CurrentRoute(r).GetName()补充,但要包recover,否则 panic 会中断日志
Body 读一次就 EOF,要复用就得复制流
想记 POST 的原始 payload?r.Body 是单次读取的 io.ReadCloser,中间件一读,后续 handler 就读到空。常见报错是 json: cannot unmarshal object into Go value —— 其实 body 已被你提前读光了。
- 安全做法:用
io.ReadAll(r.Body)读进bytes.Buffer,再用bytes.NewReader(buf.Bytes())构造新Body - 记得把新
Body赋给r.Body,并确保Close行为不丢(可包装成带自动Close的io.ReadCloser) - 如果只是记 method/path/status,完全不用碰
Body—— 别为省事加无谓开销 - 调试时用
httputil.DumpRequestOut看完整请求,别直接fmt.Printf("%+v", r),会 panic
最易忽略的是:所有中间件共享同一个 http.ResponseWriter,某个中间件提前 WriteHeader(401) 或 Write,后面中间件再写就会触发 http: superfluous response.WriteHeader panic。包装器必须严格控制 write 时机,且日志输出本身不能阻塞请求流——用 zap 这类结构化日志库比 fmt.Fprintf 安全得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











