直接 log.Printf(req) 会 panic,因 http.Request 含未导出字段和循环引用,且 req.Body 为单次读取流,读取后后续 handler 无法再读导致 JSON 解析失败;安全复用 body 需限流、读取、重置三步;响应状态码与大小需包装 ResponseWriter 捕获;脱敏须在业务 handler 前完成但日志在后,JSON 和 query 需结构化处理而非字符串替换。

为什么直接 log.Printf(req) 会 panic
因为 http.Request 包含大量未导出字段和内部循环引用,用 fmt.Printf 或 fmt.Sprintf("%+v", req) 打印会触发 runtime panic。更严重的是,req.Body 是单次读取的 io.ReadCloser,一旦被中间件调用 io.ReadAll(req.Body),后续 handler(比如 JSON binding)就读不到数据,表现为 json: cannot unmarshal object into Go value of type xxx 或空 body。
如何安全读取并复用 Request.Body
必须在不破坏原始流的前提下复制 body 内容用于日志。核心是三步:限流、读取、重置。
- 开头就用
http.MaxBytesReader包裹r.Body,防止恶意大请求耗尽内存,例如限制 10MB:r.Body = http.MaxBytesReader(w, r.Body, 10<em>1024</em>1024) - 用
io.ReadAll一次性读取到bytes.Buffer,得到原始字节[]byte - 用
bytes.NewReader(buf.Bytes())构造新io.ReadCloser赋给r.Body;注意要手动实现Close()方法,否则底层连接可能泄漏 - 若 Content-Type 是
application/x-www-form-urlencoded或multipart/form-data,优先走r.ParseForm()+r.PostForm,避免手动解析失败 - GET 请求的 body 通常为空,但某些客户端会塞数据,建议加判断:
if r.Method != "GET" && r.ContentLength > 0
怎么拿到响应状态码和响应体大小
http.ResponseWriter 接口不暴露 status 和 written 字节数,必须包装它。关键点在于:status 可能由 handler 隐式设为 200(没调 WriteHeader 就直接 Write),而响应大小需在每次 Write 后累加。
- 定义包装结构体,内嵌
http.ResponseWriter,带statusCode int、size int、wroteHeader bool -
WriteHeader(code)中更新statusCode = code并设wroteHeader = true -
Write(b)中,若!wroteHeader,先调rw.ResponseWriter.WriteHeader(http.StatusOK),再设statusCode = 200 - 所有写入通过
io.MultiWriter(rw.ResponseWriter, &sizeBuffer)同步写入原始 writer 和计数 buffer
脱敏逻辑该放在中间件的哪个位置
脱敏必须在业务 handler 执行前完成,否则 body 已被消费;但日志行(含 status、耗时)必须在 next.ServeHTTP 之后打。这是最易错的时间窗口。
立即学习“go语言免费学习笔记(深入)”;
- 对 JSON 请求:先用
json.Unmarshal解析为map[string]interface{},递归遍历 key,命中敏感键(如"password"、"token"、"phone")则将对应字符串 value 替换为"***";非字符串类型(数字、bool)不动 - 对 query 参数:用
r.URL.Query()获取后遍历,脱敏后需同步更新r.URL.RawQuery,否则日志里还是明文 - 敏感键列表不要硬编码,从配置文件加载,支持正则(如
^.*key$),避免漏掉auth_token或idCard - 不要对整个 JSON 字符串做
strings.ReplaceAll—— 容易破坏结构或误杀字段名


















