日志中间件中不能直接读 r.Body,因其为单次读取的 io.ReadCloser,中间件读取后 handler 再读将得 EOF,导致 JSON 解析 panic 或 json.RawMessage 为空;必须复制 body 流,如用 bytes.Buffer + io.MultiReader 或先 io.ReadAll 再 bytes.NewReader,并手动实现 Close() 防连接泄漏。

为什么日志中间件里不能直接读 r.Body
因为 r.Body 是 io.ReadCloser,底层是单次读取流——中间件一调用 io.ReadAll(r.Body),后续 handler 再读就只能拿到 EOF。常见后果是业务 handler 解析 JSON 时 panic:json: cannot unmarshal object into Go value of type xxx,或 json.RawMessage 为空。
必须复制 body 流才能复用。推荐用 bytes.Buffer + io.MultiReader 或 io.TeeReader:
- 若只记录 method/path/remote addr 等 header 信息,完全不用碰
r.Body,安全 - 若要记录原始 payload(比如调试 POST JSON),先
bodyBytes, _ := io.ReadAll(r.Body),再用bytes.NewReader(bodyBytes)构造新r.Body - 注意:新
io.ReadCloser需手动实现Close(),否则可能泄漏连接
如何正确包装 http.ResponseWriter 获取状态码和响应长度
http.ResponseWriter 接口不暴露 Status 和已写入字节数,直接调 w.WriteHeader(200) 后也拿不到实际生效的 status——因为 handler 可能不显式调用它,而靠第一次 w.Write() 隐式触发 200。
必须自己实现 wrapper 类型:
立即学习“go语言免费学习笔记(深入)”;
- 定义结构体,字段含原始
http.ResponseWriter、status int(初值 0)、written int、wroteHeader bool -
WriteHeader(statusCode int)中更新status并设wroteHeader = true -
Write(p []byte)中,若!wroteHeader,先调rw.ResponseWriter.WriteHeader(http.StatusOK),再记status = 200 - 所有写入走
io.MultiWriter,同时写向原始 writer 和一个bytes.Buffer统计长度
日志中间件中 request ID 和 trace ID 怎么传才不丢
request ID 必须在最外层生成并注入 context.Context,trace ID 则需从请求头(如 X-Trace-ID)提前读取——否则熔断、认证等中间件运行时就拿不到 ID,导致日志脱节、链路断裂。
错误做法:在日志中间件内部生成 ID 后才调 next.ServeHTTP(w, r.WithContext(ctx));此时上游中间件(如熔断器)已执行完毕,ID 对它们无效。
正确顺序(由外到内):
- 追溯中间件:读 header → 生成/透传
trace_id→ 注入ctx→ 调next.ServeHTTP - 熔断中间件:从
r.Context().Value("trace_id")取 ID 记录失败事件 → 决策是否放行 - 日志中间件:最后执行,统一记录 start/end、status、耗时、trace_id
用 fmt.Printf 打印 *http.Request 会 panic?
会。因为 http.Request 包含未导出字段、sync.Mutex、循环引用(如 r.Context().Value 可能回指 r),直接 fmt.Printf("%+v", r) 触发无限递归,runtime panic。
安全做法只有两种:
- 只取你需要的字段:如
r.Method、r.URL.Path、r.RemoteAddr、r.Header.Get("User-Agent") - 若真要 dump 全量结构,用
httputil.DumpRequest(r, false)(注意第二个参数为false避免读 body)
真正难的不是写日志中间件,而是让 body 可读又不破坏后续逻辑、让 status 可捕获又不干扰默认行为、让 context 值在整条链里始终在线——这些细节漏掉一个,日志就变成误导源。


















