需自定义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 倍。
立即学习“go语言免费学习笔记(深入)”;
- 字段顺序依赖日志格式配置(如 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 会成瓶颈。
更实用的做法是分片:用 [8]*sync.Map,key 的 hash 对 8 取模决定写入哪个分片。查总数时遍历 8 个分片的 Range 累加。内存占用略增,但写吞吐提升明显。
-
sync.Map的LoadOrStore比普通 map 加锁快,但只适合读多写少;日志统计写密集,分片更稳 - 路径聚合(如
/user/:id)需预定义规则,别在运行时用正则匹配所有路径——CPU 爆掉 - 统计结果导出别用
fmt.Sprintf拼 JSON,用json.Encoder流式写入文件或网络连接,防内存暴涨
为什么 os.OpenFile 写日志文件总卡住?
直接开 O_WRONLY | O_CREATE | O_APPEND 没问题,但若每条日志都 os.OpenFile → WriteString → Close,磁盘 I/O 会成为瓶颈,尤其机械盘。更糟的是没设 os.File.Sync,断电时最后几秒日志丢失。
正确姿势是长连接式文件句柄 + 缓冲写入:bufio.NewWriterSize(file, 64*1024),每满 64KB 或 1 秒 flush 一次。同时用 file.Chmod(0644) 确保日志可被外部工具(如 tail -f)读取。
- 别用
log.SetOutput直接塞文件,它不缓冲也不支持自动 rotate - 滚动日志要用
lumberjack.Logger(第三方),但注意它的MaxAge是基于文件修改时间,不是写入时间,时钟回拨会导致误删 - 如果日志要发 Kafka 或 ES,本地文件只是兜底,务必加失败重试队列,别丢数据
真正难的不是解析一行日志,而是当 QPS 从 100 涨到 5000 时,你的计数器不抖动、文件不卡死、内存不持续增长——这些边界条件往往只在凌晨三点压测时暴露。










