必须包装 ResponseWriter 才能获取真实状态码和响应体大小,否则日志无效;需重写 WriteHeader 和 Write 方法,并为每个请求新建包装实例以避免并发错误。

不包装 ResponseWriter 就拿不到真实状态码和响应体大小,日志就等于废的。
为什么必须重写 WriteHeader 和 Write
标准 http.ResponseWriter 是个接口,没提供 StatusCode() 或已写入字节数。硬编码状态码(比如写死 200)会掩盖 404、500 甚至 panic 导致的空白响应。
-
WriteHeader必须拦截:显式设置的状态码只在这里出现,不捕获就永远不知道实际发了什么 -
Write必须重写:用于累加size;同时要 fallback 到200—— handler 没调WriteHeader就直接Write时,底层默认就是200 - 每个请求必须新建包装实例:复用结构体字段会导致并发下
status和size被多个 goroutine 覆盖错乱
func(http.Handler) http.Handler 是唯一安全的中间件签名
写成 func(http.HandlerFunc) http.HandlerFunc 看似能跑,但嵌套两层就会编译失败 —— Go 不自动做接口转换。
- 返回值必须是
http.Handler,哪怕内部用http.HandlerFunc包逻辑再 return - 注册时必须用
http.Handle或srvMux.Handle;http.HandleFunc只收裸函数,接不住已包装的中间件 - 链式组合时顺序是:最外层中间件最先执行,所以熔断应比日志靠外,trace ID 注入要最早
哪些字段不记等于白记
只记 r.Method 和 r.URL.Path 是半残废日志。生产环境至少得有这五项:
立即学习“go语言免费学习笔记(深入)”;
- 耗时:用
time.Since(start),别用Format字符串拼接,避免格式化开销 - 状态码:必须来自包装器的
status字段,不是r.URL解析结果 - 响应体大小:来自包装器的
size字段,不是len([]byte)硬估 - 客户端 IP:优先用
r.RemoteAddr,若在反向代理后,需从X-Forwarded-For解析并校验可信跳数 - 原始路径:用
r.RequestURI,比r.URL.Path更可靠(含 query,未被 URL 解码) - 敏感参数如
?token=xxx必须脱敏,否则日志泄露凭证;脱敏不能靠字符串匹配,得结合 method/path/Content-Type 做结构化解析
要不要读 r.Body?怎么读才不破坏后续 handler
除非业务强依赖原始 payload(比如审计、重放),否则不要碰 r.Body。一读就 EOF,下游 handler 会报 json: cannot unmarshal object into Go value。
- 真要读,必须:
bodyBytes, _ := io.ReadAll(r.Body)→r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 同步更新
r.ContentLength = int64(len(bodyBytes)),否则某些框架(如 gin binding)会出错 - 开头必须加
http.MaxBytesReader限流,比如r.Body = http.MaxBytesReader(w, r.Body, 1,防 OOM - GET 请求 body 大概率为空,但部分客户端会塞数据,建议按
r.Method == "GET"显式跳过
最常被忽略的是:日志中间件里用 r.Context() 提取 trace ID 时,如果追溯中间件没在它之前注入,ID 就是空的;而这个错误不会 panic,只会静默丢失追踪能力。


















