用 http.handler 包装实现日志中间件,记录 method、path、status、duration_ms 四个字段,使用结构化日志库(如 zap),避免敏感信息泄露、i/o 阻塞和上下文污染。

用 http.Handler 包一层就能记日志
Go 的 HTTP 服务天然适合用中间件模式加日志,不用改路由定义,也不用每个 http.HandleFunc 里重复写。核心就是自己实现一个包装 http.Handler 的结构体,把原始 handler 包进去,在调用前后读取时间、方法、路径。
常见错误是直接在 ServeHTTP 里用 r.URL.Path 但没考虑路由变量(比如 /user/{id})——实际记录的还是原始路径,不是模板路径。所以日志里的路径应以 r.Method + " " + r.URL.Path 为准,别试图还原路由规则。
- 必须用
time.Now()在handler.ServeHTTP前后各打一次点,不能只靠defer闭包捕获开始时间(容易被协程调度影响) - 耗时建议用
time.Since(start),单位选time.Millisecond并四舍五入到整数,避免小数点后一堆零干扰排查 - 别在日志里拼接完整 URL(含 query),query 可能含敏感参数;只记路径和 method 更安全
log.Printf 不够用,得用结构化日志库
原生 log.Printf 输出是纯文本,查日志时没法按 method 或耗时范围过滤。生产环境建议上 zap 或 zerolog,哪怕只是轻量接入,也能让字段对齐、时间带毫秒、支持 JSON 输出。
典型坑是日志字段名不统一:有人写 method,有人写 http_method,导致 ELK 或 Loki 里聚合困难。定好规范,比如固定用 method、path、status、duration_ms 这四个字段。
-
zap初始化时用zap.WithCaller(false)关掉文件行号,避免污染请求上下文 - 如果用了
gorilla/mux,可通过r.Context().Value()拿到匹配后的路由名(需提前注入),但别依赖它生成日志主键——没匹配上时会 panic - 日志级别统一用
Info,别对 200/404 做分级;异常响应(5xx)由业务层单独打 Error 日志
别漏掉 panic 和超时请求的日志
标准 handler 包裹只能捕获正常流程,一旦中间件或业务逻辑 panic,或者客户端断连导致 context.DeadlineExceeded,默认就静默了。这类请求不记录,等于监控盲区。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
最简方案是在日志 handler 末尾加 recover(),并检查 w.(interface{ Written() bool }).Written() 判断是否已写 header(防止重复写)。但要注意:recover 后不能再写 response body,只能记日志。
- panic 日志必须包含
stacktrace,用debug.Stack()获取,否则光看 “interface conversion: http.ResponseWriter is …” 完全没法定位 - 超时请求的耗时字段要显式设为
-1或"timeout",别让它和慢请求混在一起;可额外加error="context deadline exceeded" - 别在 recover 里尝试 respone.WriteHeader(500) —— 此时 connection 很可能已关闭,会触发 “write on closed connection” 错误
高并发下 log 写磁盘会拖慢接口
本地开发用 log.Printf 没问题,但线上 QPS 上千后,频繁系统调用写文件会让 P99 耗时跳变。不是日志内容的问题,是 I/O 阻塞了 goroutine。
解决方式不是关日志,而是换输出目标:把日志写到 os.Stdout,交给 systemd/docker 日志驱动做异步刷盘;或者用 zap.NewProductionConfig().EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 开启 JSON 格式,方便日志采集器解析。
- 绝对不要用
log.SetOutput(&os.File{})直接写文件,尤其没配 rotate —— 磁盘打满是分分钟的事 - 如果必须本地落盘,用
lumberjack.Logger做切割,但注意它的MaxSize单位是 MB,不是 KB - 测试时用
ab -n 1000 -c 100对比加日志前后的 p95 耗时,差值超过 2ms 就得查 I/O 是否成瓶颈
真正难的不是记下请求,而是让日志不干扰请求本身。很多团队卡在“加了日志,接口变慢”,其实问题不在日志内容,而在输出方式和上下文清理是否干净。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










