log.printf 会丢状态码和路径,因为 r.url.path 在代理或重写后可能被改写为“/”,真实路径需用未解析的 r.requesturi 获取;状态码须包装 responsewriter 拦截 writeheader 才能准确捕获。

为什么直接 log.Printf 会丢状态码和路径
因为 http.Request.URL.Path 在反向代理、URL 重写或某些中间件(如 gorilla/mux)后可能被改写成 /,而真实请求路径藏在 r.RequestURI 里——它是服务端收到的原始字符串,没被标准库解析篡改过。但注意它带查询参数,敏感字段得脱敏;别用 r.URL.String(),它会补 ?、漏 fragment。
状态码更麻烦:WriteHeader 可能被业务 handler 延迟调用,甚至不调(Go 默认隐式设为 200),所以不能在 next.ServeHTTP 前读 w.WriteHeader —— 必须包装 ResponseWriter 拦截 WriteHeader 和 Write 调用。
如何安全读取并复用 request.Body
r.Body 是单次读取的 io.ReadCloser,中间件里 io.ReadAll(r.Body) 一次,后续 handler 就读到 EOF,常见报错是 json: cannot unmarshal object into Go value 或 binding 失败。
- 必须先用
http.MaxBytesReader(nil, r.Body, 10<code>MB) 限流防 OOM - 再
io.ReadAll到[]byte,然后用bytes.NewReader(buf)构造新 body 塞回r.Body - 同步更新
r.ContentLength = int64(len(buf))和r.Header.Set("Content-Length", strconv.Itoa(len(buf))),否则gin.BindJSON等会出错 - GET 请求的 body 通常为空,但有些客户端会塞数据,建议加判断
r.Method != "GET"再读
结构化日志该记哪些字段、怎么脱敏
只记 method、path、status、latency 不够,线上排查需要更多上下文:客户端 IP(r.RemoteAddr)、trace ID(从 r.Header.Get("X-Request-ID") 或 context 取)、原始 query(r.URL.RawQuery)、脱敏后的 body。
脱敏不能靠关键词黑名单硬匹配(比如只筛 "password"),因为字段名可能变形("pwd"、"auth_token"、"idCard")。实际应结合:
-
r.Method和r.URL.Path判断是否为敏感接口(如POST /api/v1/login) -
r.Header.Get("Content-Type")判断格式(application/json就递归遍历 map[string]interface{},只替换字符串类型的敏感 key 对应的 value) - query 参数用
r.URL.Query()遍历,脱敏后要同步更新r.URL.RawQuery,否则日志里看到的是未脱敏串 - 敏感键列表建议从配置文件加载,支持正则(如
^.*token$),避免硬编码
gin 框架里注册和控制粒度的关键点
gin 的中间件签名是 gin.HandlerFunc,必须返回 func(*gin.Context),且日志写入时机有严格顺序:
- 脱敏逻辑必须在
c.Next()之前——此时c.Request.Body还没被消费 - 状态码、响应大小、耗时必须在
c.Next()之后读——调用c.Writer.Status()、c.Writer.Size()、time.Since(start) - 别在日志里拼接
c.Request.Body字符串,它已关闭;要记录 body 得提前缓存副本 - 若用
zap,记得用c.Request.Context()传 trace ID,避免并发日志交错
最易忽略的是:没包装 ResponseWriter 就直接读 c.Writer.Status(),在某些 gin 版本里会返回 0;还有就是忘记对 multipart/form-data 类型走 c.Request.ParseMultipartForm(),硬解析 JSON 会失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











