需自定义http.Handler和ResponseWriter包装器来结构化日志,记录方法、路径、IP、状态码、耗时等;解析用strings.FieldsFunc+Trim引号;并发统计用分片sync.Map;日志写入需缓冲与flush;滚动与导出需第三方库及兜底重试。
如何用 net/http 拦截并结构化 Web 访问日志
Go 标准库不自带访问日志中间件,得自己封装 http.Handler。核心是包装原 handler,在 ServeHTTP 中读取 Request 字段(r.Method、r.URL.Path、r.RemoteAddr)和响应状态码(需用 ResponseWriter 包装器捕获)。
常见错误:直接在 handler 里写日志但没记录响应耗时或状态码——因为 WriteHeader 和 Write 发生在 handler 执行之后,必须用自定义 responseWriter 拦截。
- 用
time.Now()记开始时间,defer算耗时 - 响应体大小无法直接获取,可统计
Write调用的字节数累加 - 避免在日志中打印
r.Header全量(含敏感 Cookie),按需提取User-Agent或Referer
日志解析阶段该用 strings.FieldsFunc 还是正则?
Nginx 或 Apache 的 access log 默认是空格分隔但带引号字段(如 "GET /api/v1/users HTTP/1.1"),纯 strings.Split 会切坏。正则虽灵活但性能差,尤其高并发下每秒千条日志时明显拖慢。
推荐组合方案:strings.FieldsFunc(line, func(r rune) bool { return r == ' ' }) 切基础字段,再对第 6、7、9 等带引号字段用 strings.Trim 去首尾引号。比全量正则快 3–5 倍。
- 字段顺序依赖日志格式配置(如 Nginx 的
log_format),务必先确认实际日志字段索引 - 时间戳字段(如
[10/Jul/2024:15:22:34 +0800])需用time.Parse配合固定 layout,别用time.ParseInLocation忽略时区导致错位 - 状态码和响应大小字段可能为破折号
-(如 400 错误未返回 body),解析前要strings.TrimSpace并检查空值
用 map[string]int 做实时统计会遇到什么并发问题?
多个 goroutine 同时写同一个 map 会 panic:”fatal error: concurrent map writes”。不能靠加锁包一层就完事——高频写入(如每秒万级请求)下 sync.RWMutex 会成瓶颈。