日志中间件必须置于中间件链末端(trace→circuitbreaker→logging→handler),因其需完整上下文(trace id、状态码、耗时等);须用statusrecorder包装responsewriter捕获真实状态码与响应大小,并加defer/recover防止panic导致日志丢失。

日志中间件必须在请求生命周期的最外层包裹,但不能放在熔断或链路追踪之前——否则可能记录不到 trace ID 或误记熔断拦截事件。
为什么日志中间件要放在中间件链末端
日志中间件需要完整上下文才能准确记录:状态码、响应耗时、trace ID、用户身份等。如果把它放在 CircuitBreaker 之前,熔断拦截后 next.ServeHTTP() 根本不执行,日志里就看不到“被拒绝”的事实;如果放在 TraceMiddleware 之后但没做 context 透传,r.Context().Value("trace_id") 可能是 nil。
- 正确顺序应为:
TraceMiddleware→CircuitBreaker→LoggingMiddleware→handler - 日志中间件只读取 context 和响应结果,不修改请求流或提前返回
- 它依赖
statusRecorder包装原始http.ResponseWriter,才能捕获真实状态码和响应体大小
如何实现可记录状态码与响应大小的日志中间件
直接用 w.WriteHeader() 不可靠——业务 handler 可能不显式调用,Go 默认返回 200,但你无法在 ServeHTTP 返回后拿到这个值。必须用包装器劫持写操作。
- 定义
statusRecorder结构体,嵌入http.ResponseWriter并重写WriteHeader和Write - 在
WriteHeader中缓存status,在Write中累加size - 日志打印时机放在
next.ServeHTTP(w, r)之后,此时耗时、状态码、大小均已确定
type statusRecorder struct {
http.ResponseWriter
status int
size int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *statusRecorder) Write(b []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.ResponseWriter.Write(b)
r.size += n
return n, err
}
常见错误:漏掉 panic 恢复导致日志中断
如果业务 handler 内部 panic,整个 goroutine 会终止,next.ServeHTTP() 后的日志语句永远不会执行——你看到的只是半截请求记录,甚至完全没日志。
- 必须在日志中间件中加
defer/recover,确保即使 panic 也能输出错误行和耗时 - 不要只 recover 不处理:至少记录
"panic recovered: %v"和堆栈 - 注意:recover 只对当前 goroutine 有效,别指望它能跨 goroutine 捕获
日志字段要不要写进 context?
不需要。request_id、trace_id 这类标识应该由更上游的中间件(如 TraceMiddleware)注入 context;日志中间件只负责「读取」,不是「生成」。强行在日志中间件里生成 request_id 会导致:多个中间件各自生成 ID,下游拿到的和日志里写的不一致。
真正容易被忽略的是:日志中间件必须兼容 context.WithValue 传递的键名约定。比如 trace 中间件用 "trace_id",你就得用 r.Context().Value("trace_id") 去取,而不是硬编码 "X-Trace-ID" ——后者是 header 名,不是 context key。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











