必须包装responsewriter才能捕获真实状态码,因原生接口不暴露已写入状态;需自定义结构体嵌入原响应器,重写writeheader存code、write中fallback默认200,且避免重复调用writeheader。

怎么包装 ResponseWriter 捕获真实状态码
不包装 ResponseWriter 就拿不到最终写入的状态码——r.URL.Path 和 time.Since() 都好办,唯独状态码容易错:handler 里没调 WriteHeader 时,默认是 200,但标准库不会主动暴露这个值;直接读 w 接口字段会 panic。
必须实现自定义结构体,重写 WriteHeader 和 Write:
-
WriteHeader中存下code并赋给statusCode字段 -
Write中检查statusCode == 0,为真则 fallback 到http.StatusOK(HTTP 规范要求首次写 body 前隐式设 200) - 绝不能在
Write里再调WriteHeader,会违反 HTTP 协议,触发panic: multiple response.WriteHeader calls
示例关键片段:
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
size int
}
func (w *responseWriterWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
func (w *responseWriterWrapper) Write(b []byte) (int, error) {
if w.statusCode == 0 {
w.statusCode = http.StatusOK
}
n, err := w.ResponseWriter.Write(b)
w.size += n
return n, err
}
为什么 r.URL.Path 不可靠,该用哪个字段记路径
反向代理、URL 重写、或某些网关(如 Envoy、Nginx with proxy_pass)会修改 r.URL 结构体,导致 r.URL.Path 变成 / 或被解码/标准化,丢失原始语义。日志归档和链路追踪需要的是服务端收到的“原样字符串”。
唯一稳定可依赖的是 r.RequestURI:
- 它来自底层 TCP 连接原始字节,未被
net/http解析或修改 - 包含完整路径 + 查询参数(如
/api/users?id=123&token=abc) - 注意:敏感参数需脱敏后再记录,不能直接打全量;脱敏后也应同步更新日志中显示的
RequestURI字段,避免误导 - 别用
r.URL.String()—— 它可能补?、漏 fragment、甚至把空查询参数转成?&
如何安全读取并复用 request.Body 而不破坏后续 handler
r.Body 是单次读取流,中间件里 io.ReadAll(r.Body) 后,下游 handler 再读就得到空内容,尤其影响 json.Decode、form.Parse 等逻辑。
正确做法分三步:
- 用
http.MaxBytesReader包一层,限制最大读取长度(如 1MB),防 OOM - 读出
[]byte后,用bytes.NewReader重建r.Body - 手动同步设置
r.ContentLength和r.Header.Set("Content-Length", ...),否则某些框架(如 Gin 的 binding)会因长度不一致拒绝解析 - GET 请求通常无 body,但部分客户端会塞数据;建议加判断:
if r.Method != "GET" && r.ContentLength > 0
脱敏也必须在这一步做:只对 JSON 或表单中的字符串 value 替换,保留原始类型(数字、bool 不处理),且敏感 key 应从配置加载,支持正则匹配(如 ^.*token$)。
JSON 日志里中文变 \u4f60\u597d 怎么禁用 Unicode 转义
json.Marshal 默认对所有非 ASCII 字符转义,不是 bug,是设计行为。但生产日志里全是 \u 码,排查效率极低,ES/Kibana 也难检索。
两种实用解法:
- 用
json.Encoder替代json.Marshal,调enc.SetEscapeHTML(false)—— 这仅禁用<code>>&转义,不影响 Unicode - 要彻底禁用 Unicode 转义,得自己实现
MergeJSON或用go-json库;或者改用zap,配合zap.Stringer+ 自定义String()方法,性能更高、无转义问题 - 千万别给日志结构体字段加
json:",string"标签——会导致数字变成字符串,ES 里被当 keyword 类型,无法聚合统计
真正容易被忽略的点:时间戳别用 time.Now().Format("2006-01-02T15:04:05.000Z07:00") 拼接,开销大;直接用 time.Now().UnixMicro() 存整数,查的时候再格式化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











