http.handler链中不能直接用log.println打印请求体,因为r.body是单次读取的io.readcloser,中间件读取后后续handler将读到eof;需用bytes.buffer+io.multireader或io.teereader复制流以复用body。

为什么 http.Handler 链里不能直接用 log.Println 打印请求信息
因为 http.Request.Body 是 io.ReadCloser,读一次就 EOF,中间件里调用 r.Body.Read()(比如为了记录请求体)会导致后续 handler 拿不到数据。Go 的 HTTP 标准库不自动缓存 body,你得自己处理。
常见错误现象:json: cannot unmarshal object into Go value of type xxx,或后端解析到空 body —— 其实是日志中间件提前把 body 读光了。
- 必须用
io.TeeReader或io.MultiReader+bytes.Buffer复制 body 流,让后续 handler 还能读 - 如果只记录 method、path、status,不用读 body,那可以直接记,但 status 只有在写 response 后才确定,需包装
http.ResponseWriter - 别用
fmt.Printf直接打 struct,http.Request里有大量未导出字段和循环引用,会 panic
如何包装 ResponseWriter 拦截状态码和响应长度
标准 http.ResponseWriter 不暴露 status 和 written 字节数,必须实现一个 wrapper 类型,重写 WriteHeader、Write 和 WriteHeader 调用时机的记录逻辑。
关键点:status 必须在第一次 Write 或显式 WriteHeader 后才真正生效;如果 handler 没调 WriteHeader 就写了 body,Go 默认用 200,但 wrapper 得能捕获这个隐式行为。
- 定义结构体包含原始
http.ResponseWriter、status(初始设为 0)、written(初始 0)和wroteHeader(bool) -
WriteHeader方法里更新status并标记wroteHeader = true -
Write方法中,若!wroteHeader,先调rw.ResponseWriter.WriteHeader(http.StatusOK)再记录status = 200 - 所有写入都通过
io.MultiWriter同时写向原始 writer 和一个bytes.Buffer(用于统计字节数)
怎么安全读取并复用 Request.Body
想记录 POST JSON 的原始 payload?必须把 body 读出来、存一份、再塞回 request——但 r.Body 是单次读取流,不能“重置”。Go 1.19+ 提供了 r.Clone(),但 clone 出来的 request body 仍是空的,除非你手动赋值。
最稳妥的做法:用 io.ReadAll 一次性读完 body 到 bytes.Buffer,然后用 bytes.NewReader(buf.Bytes()) 构造新 body,并赋给 r.Body;但注意这会丢失原始 body 的 Close 行为,需额外包装。
- 推荐封装一个
ReadBody工具函数,返回[]byte和新的io.ReadCloser - 新
ReadCloser应在Read后自动Close,避免泄漏;可用io.NopCloser包一层bytes.NewReader - 如果请求体很大(如文件上传),不要全读进内存,改用
io.TeeReader(r.Body, logWriter)边读边记,但此时无法获取完整内容用于调试 - 记得检查
r.ContentLength,超限直接拒绝,防止 OOM
一个轻量但生产可用的日志中间件示例
下面这段代码绕过了第三方依赖,只用标准库,支持 method、path、status、耗时、body 长度,且不破坏原始 flow:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &responseWriter{ResponseWriter: w, status: 0}
// 读 body(仅当需要记录时)
var bodyBytes []byte
if r.ContentLength > 0 && r.ContentLength
<p>真实项目里要注意:<code>log.Printf</code> 是同步阻塞的,高并发下会拖慢吞吐;建议用结构化日志库(如 <code>zap</code>)并异步写;另外,敏感字段(如 Authorization header、password 字段)必须脱敏,不能直接打印 raw body。</p>











