Go自定义日志中间件核心是准确捕获请求起止时间、上下文与数据:需用func(http.Handler) http.Handler签名,显式调用next.ServeHTTP,单次time.Now()计时,从Header取request ID,缓冲并重设Body,包装ResponseWriter以记录状态码和响应大小。

Go 自定义日志中间件的核心不是“打日志”,而是“在正确时机、用正确上下文、拿到正确数据”——漏掉任意一环,日志就只剩时间戳和路径,失去排查价值。
func(http.Handler) http.Handler 是唯一合法签名
写成 func(w http.ResponseWriter, r *http.Request) 或 func() 都无法接入 HTTP 流程。必须返回一个符合标准的 http.Handler,且接收上一级 handler 作为参数:
- 错误写法:
func logging(w http.ResponseWriter, r *http.Request)—— 这只是普通函数,http.Handle和gin.Use都不认识它 - 正确结构:外层接收配置(如
*log.Logger),中层返回func(http.Handler) http.Handler,内层用http.HandlerFunc包装逻辑 - 关键动作:必须显式调用
next.ServeHTTP(w, r),否则请求卡死;调用位置决定是前置、后置还是拦截
耗时计算必须用 time.Since(start),不能两次 time.Now()
高并发下分别调用 time.Now() 可能跨调度周期,导致 latency 为负或异常偏大。起始时间必须在进入 handler 时只取一次,并透传到结束点:
- 正确:
start := time.Now()→defer func() { log.Printf("cost: %v", time.Since(start)) }() - 错误:
start := time.Now()→ 中间一堆逻辑 →end := time.Now()→end.Sub(start) - 注意:别依赖日志库自身的 timestamp 字段——它记录的是
log.Print执行时刻,不是请求处理起止点
request ID 和 trace_id 必须从 Header 直接取,别用 ctx.Value 传
ctx.Value 容易被上游中间件覆盖或遗漏,尤其在嵌套 goroutine 或第三方库调用时不可靠。真实生产环境应优先从请求头提取:
立即学习“go语言免费学习笔记(深入)”;
- 推荐方式:
req.Header.Get("X-Request-ID")或req.Header.Get("Trace-ID") - 不推荐:
req.Context().Value(TraceIDKey)—— 除非你 100% 控制所有中间件都注入且不覆盖 - 生成新 ID 时用
uuid.NewString()(Go 1.20+),别用rand.Int63(),QPS 超过 10k 时碰撞风险陡增
要记录请求体或 query,必须提前复制并重设 Body
http.Request.Body 是单次读取的,io.ReadAll(r.Body) 后下游 handler 就读不到数据了。必须缓冲并重装:
- 安全做法:用
http.MaxBytesReader限流(如 1MB),再io.ReadAll到[]byte,然后r.Body = bytes.NewReader(buf) - 别忘同步更新:
r.ContentLength = int64(len(buf)),并重设Content-Lengthheader,否则gin.BindJSON等会失败 - GET 请求一般无 body,但有些客户端会塞数据,建议加判断:
if r.Method != "GET" { ... } - query 脱敏需操作
r.URL.Query(),同时更新r.URL.RawQuery,否则日志里看到的仍是原始串
最常被忽略的其实是响应体大小和状态码的准确捕获——这要求你包装 http.ResponseWriter,实现自己的 WriteHeader 和 Write 方法,并在其中记录 status 和累计 size。没做这一步,所谓“全链路日志”就缺了最关键的一块拼图。


















