go http日志中间件需包装http.responsewriter以捕获状态码、字节数和耗时,不能仅用log.printf;应返回func(http.handler)http.handler闭包,记录method/path/status/size/duration_ms等字段,生产环境须采样且避免解析body。

Go 的 HTTP 中间件本身不内置日志能力,必须手动封装 http.Handler 或用 http.HandlerFunc 包裹原始 handler,在调用前后注入日志逻辑——这不是“配置”,而是函数组合。
为什么不能直接用 log.Printf 做中间件
单纯在 handler 里写 log.Printf 只能打固定格式、无法获取请求耗时、响应状态码、路径参数等上下文;更关键的是,它发生在 handler 执行中,拿不到响应体大小和真实写入状态(比如 client 提前断连导致 WriteHeader 失败但日志已输出)。真正的中间件需劫持 http.ResponseWriter 实现拦截。
- 必须包装
http.ResponseWriter,重写WriteHeader和Write方法才能捕获状态码与字节数 - 记录开始时间要放在 wrapper 创建前,否则有纳秒级偏差
- 别用
time.Since在 defer 里算耗时——如果 handler panic,defer 仍执行,但可能没写 header,日志里状态码会是 0
如何实现可复用的 logging middleware 函数
推荐返回 func(http.Handler) http.Handler 类型的闭包,方便链式使用(如套在 cors、auth 外层)。核心是定义一个实现 http.ResponseWriter 接口的结构体,携带状态码、字节数、是否已写 header 等字段。
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
written bool
size int
}
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.written = true
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *responseWriterWrapper) Write(b []byte) (int, error) {
if !w.written {
w.statusCode = http.StatusOK
w.written = true
}
n, err := w.ResponseWriter.Write(b)
w.size += n
return n, err
}
中间件函数本身只需提取 request ID(如从 X-Request-ID)、记录起始时间、包装 response writer、调用 next handler、最后打日志行:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 用
ctx.Value传 request ID 不推荐——容易被上层覆盖;建议用req.Header.Get("X-Request-ID")直接取 - 日志字段建议至少包含:
method,path,status,size,duration_ms,user_agent(可选) - 避免在日志里拼接完整 URL(含 query),防止敏感参数泄露;用
req.URL.Path即可
在 Gin / Echo / Fiber 中怎么接入
各框架对中间件签名约定不同,但底层都是把 http.Handler 转成自身 handler 类型。不要试图“适配所有框架”,而应按框架文档写对应版本:
- Gin:接收
gin.HandlerFunc,需把标准中间件转为func(*gin.Context),用c.Writer获取底层http.ResponseWriter并包装 - Echo:用
echo.MiddlewareFunc,通过c.Response().Writer拿到http.ResponseWriter,注意 Echo v4+ 默认禁用直接写 header,需调用c.Response().Before()注册钩子 - Fiber:原生不暴露
http.ResponseWriter,必须用c.Context.Fasthttp.Response.Body()+ 自定义ctx.SetUserValue记录状态,不如前两者直观
如果你用的是标准 net/http,最简接入就是:http.ListenAndServe(":8080", loggingMiddleware(yourHandler))。
性能与采样注意事项
高频服务下,每请求都打全量日志会显著拖慢吞吐,尤其当磁盘 I/O 或网络日志后端延迟高时。生产环境必须加控制:
- 默认只记录
status >= 400或duration_ms > 500的慢请求 - 用原子计数器做简单采样,比如
if rand.Intn(100) == 0记一条(开发期可用,线上建议用更稳的哈希采样) - 避免在日志里调用
req.FormValue或解析 body——这会 consume request body,导致下游 handler 读不到数据 - 结构化日志(如 JSON)比文本日志解析成本高 2–3 倍,若用 Loki/ELK,优先考虑预定义字段名 + 文本格式
真正难的不是写出能跑的日志中间件,而是让日志字段对齐可观测性需求:trace ID 关联、错误分类标记、业务上下文注入——这些得靠中间件外的 context 传递和业务代码配合,不是加几行 log 就能解决的。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










