最稳妥是在 http handler 外层套 wrapper 函数,实现 http.handler 接口的 servehttp 方法并包装 responsewriter 以捕获状态码,用 r.requesturi 获取原始路径,json 日志禁用 unicode 转义,关键字段同步记、非关键字段异步采样。

HTTP handler 里怎么加日志不破坏原有逻辑
直接套一层 wrapper 函数最稳妥,别动原 handler 的签名或返回值。http.Handler 接口就一个 ServeHTTP 方法,日志逻辑塞进去就行,不影响路由注册和中间件链路。
常见错误是试图在 handler 内部用 log.Printf 打印后继续调用 next.ServeHTTP,结果 panic:因为没把 ResponseWriter 包装好,写响应时被提前关闭或双写。
- 必须用
httputil.DumpRequestOut或手动读取r.Body(注意重放)看请求体,别直接打印r指针 - 记录响应状态码得靠包装
http.ResponseWriter,实现WriteHeader和Write方法捕获真实状态 - 时间戳用
time.Now().UnixMicro(),别用Format字符串拼接,避免格式化开销
为什么 log.Println 在中间件里总打不出请求路径
因为 http.Request 的 URL 字段在某些代理或重写场景下不完整,比如反向代理后 r.URL.Path 可能是 /,而真实路径藏在 X-Forwarded-Path 或原始 RequestURI 里。
更可靠的是用 r.RequestURI —— 它是服务端收到的原始字符串,没被标准库解析篡改过,适合日志归档。但要注意它带查询参数,敏感字段得脱敏。
- 别依赖
r.URL.String(),它会把空查询参数补成?,还可能漏掉 fragment - 如果用了
gorilla/mux,可以用mux.CurrentRoute(r).GetName()补充路由名,但别在日志里直接调用,避免 panic - 并发下
r.Header是安全的,但r.FormValue会触发ParseForm,可能修改r.Body,日志里慎用
JSON 日志格式里怎么避免 escape 字符爆炸
Go 标准 json.Marshal 默认对所有非 ASCII 字符转义,日志里中文变 \u4f60\u597d,查起来费劲。不是 bug,是默认行为,但生产日志真没必要。
用 json.Encoder 替代 json.Marshal,调用 SetEscapeHTML(false) 就行。注意这个只影响 HTML 特殊字符( <code>> &),不影响 Unicode;要彻底禁用 Unicode 转义,得自己实现 MarshalJSON 或用第三方库如 go-json。
- 别在日志结构体字段上加
json:",string"标签,会导致数字被引号包住,ES/Kibana 解析成 keyword 类型 - 如果用
zap,直接上zap.Stringer+ 自定义String()方法比 JSON 序列化快得多 - 日志行末尾别加换行符
\n,log.Output已经处理了,重复加会导致日志平台切行错乱
高并发下日志中间件拖慢接口怎么办
核心瓶颈不在打印,而在序列化和 I/O。一次请求日志如果包含完整 body、header、堆栈,序列化耗时可能达毫秒级,QPS 上千时直接拖垮 P99。
解决方案不是关日志,而是分层:关键字段(method、path、status、latency)同步记,body 和 header 异步采样写。用 sync.Pool 复用 bytes.Buffer 和 map,避免每次分配。
- 别用
fmt.Sprintf拼接日志字符串,分配多、GC 压力大;用strings.Builder或预分配[]byte - 采样率设成可配置变量,上线后通过
atomic.LoadUint32动态调整,别硬编码rand.Float64() - 写文件时用
os.OpenFile配os.O_APPEND | os.O_WRONLY,别用O_TRUNC,否则日志轮转时会丢数据
最常被忽略的是:日志中间件不该做任何阻塞操作,比如调用外部 HTTP 服务、查数据库、甚至读本地配置文件。这些都得挪到异步 goroutine 里,且必须带超时和错误丢弃逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











