Go中可用http.Handler包装器实现请求日志中间件,需拦截ResponseWriter获取状态码与响应大小,注意Body读取安全、敏感字段过滤、结构化日志(如zap)、日志轮转(lumberjack)及语义一致性。

用 net/http 中间件记录 HTTP 请求日志
Go 标准库没有内置请求日志中间件,但可以用 http.Handler 包装器轻松实现。核心是拦截 http.ResponseWriter,捕获状态码和响应体大小,并在请求结束时打日志。
常见错误是直接读取 Request.Body 导致后续 handler 无法读取——必须用 io.TeeReader 或先 io.ReadAll 再重置 Body(仅限小请求)。
- 记录字段建议包含:
method、path、status、latency_ms、user_agent、remote_ip(注意 X-Forwarded-For 处理) - 避免在日志中打印完整
body,尤其含敏感字段(如 password、token);如需调试,可只打Content-Length或采样 1% 请求 - 高并发下频繁调用
time.Now()开销可控,但用start := time.Now().UnixMicro()比time.Since()略快
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &loggingResponseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(lw, r)
latency := time.Since(start).Microseconds()
log.Printf("[%s] %s %s %d %dµs %s",
r.RemoteAddr, r.Method, r.URL.Path,
lw.status, latency, r.UserAgent())
})
}
type loggingResponseWriter struct {
http.ResponseWriter
status int
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.status = code
lrw.ResponseWriter.WriteHeader(code)
}
用 zap 替代 log.Printf 提升性能与结构化能力
log.Printf 是同步、无缓冲、无字段支持的,压测时容易成为瓶颈。zap 的 Logger 是零分配、异步可选、支持结构化字段的日志库,适合生产环境。
容易踩的坑:未调用 logger.Sync() 就退出进程,导致最后几条日志丢失;或误用 zap.String("body", string(b)) 对大 body 做字符串转换引发 OOM。
立即学习“go语言免费学习笔记(深入)”;
- 初始化时用
zap.NewProduction()获取预设 JSON encoder + error output;开发期可用zap.NewDevelopment() - 结构化字段优于拼接字符串,例如:
logger.Info("http request", zap.String("path", r.URL.Path), zap.Int("status", lw.status)) - 敏感字段(如
Authorizationheader)必须显式过滤,zap.String("auth", "REDACTED")比留空更安全
采集请求 Body 和 Query 参数需按场景谨慎开启
不是所有接口都需要记录 body —— 文件上传、GraphQL 查询、JSON API 的 POST/PUT 请求才真正需要。盲目记录会拖慢吞吐、暴露敏感数据、撑爆磁盘。
Query 参数相对安全,但要注意 password、token、code 类参数是否出现在 URL 中(OAuth callback 场景常见)。
- 读取
Body前必须检查r.ContentLength,超 1MB 直接跳过,避免内存暴涨 - 用
r.ParseForm()后读r.PostForm比直接io.ReadAll(r.Body)更兼容application/x-www-form-urlencoded - Query 可用
r.URL.Query()获取url.Values,遍历 key 时排除已知敏感键名
日志落地与轮转:用 lumberjack 防止磁盘写满
Go 标准 os.File 不支持自动切割,长期运行服务若直写单个文件,极易占满磁盘。必须引入轮转能力。
lumberjack 是最轻量且稳定的选择,但要注意它不处理多进程并发写(单进程 Go 服务没问题),且 MaxAge 是“至少保留多少天”,不是“精确删除”。
- 关键配置项:
MaxSize(单位 MB,推荐 100–500)、MaxBackups(保留几个旧文件,建议 7)、MaxAge(天数,建议 28) - 不要把日志写到
/tmp或容器/dev/shm,这些路径可能无持久性或空间受限 - 如果对接 ELK,建议额外加一个
filebeatsidecar 或用zapcore.AddSync(&filebeatWriter{})直推,避免落盘再采集的延迟
复杂点往往不在代码本身,而在于日志字段语义是否统一、敏感信息是否真的被过滤、轮转后旧日志是否能被及时归档或压缩。这些细节没对齐,查问题时就会发现日志“好像有,又好像没”。










