默认 log 包不适合记录 http 请求,因其无上下文绑定、不支持结构化输出与字段注入、缺乏日志级别及 handler 链透传能力,导致日志混乱难追溯。

为什么默认的 log 包不适合直接记录 HTTP 请求
Go 标准库的 log 包没有上下文绑定能力,无法自动关联同一请求的多条日志;它也不支持结构化输出、字段注入(如 req_id、status_code)、日志级别区分,更没法在 handler 链中透传。直接用 log.Println() 打请求日志,会导致时间错乱、来源难追溯、排查时要翻十几屏日志拼凑一条请求流。
用 net/http 中间件 + log/slog 记录结构化请求日志
Go 1.21+ 自带的 slog 支持键值对和 Handler 自定义,配合中间件可干净地拦截请求生命周期。关键不是“打日志”,而是把请求元信息(方法、路径、耗时、状态码)作为字段写入,而不是拼字符串。
实操建议:
- 用
slog.With()绑定 request-scoped 字段,比如slog.String("method", r.Method)、slog.String("path", r.URL.Path) - 在中间件中调用
next.ServeHTTP()前记录开始时间,之后计算duration并补全status_code(需包装ResponseWriter获取真实状态码) - 避免在
Defer里读r.Body—— 它可能已被下游 handler 消费,导致空内容或 panic - 若需记录请求体(如调试),应先用
io.ReadAll(r.Body),再用io.NopCloser()重置 Body,否则 handler 会收不到数据
如何获取真实 status_code 而不是默认 200
标准 http.ResponseWriter 不暴露已写入的状态码,直接 w.WriteHeader(404) 后无法回读。必须用包装器拦截 WriteHeader 调用。
实操建议:
- 定义一个结构体嵌入
http.ResponseWriter,重写WriteHeader(int)方法,把传入的 code 存到字段里 - 在中间件中创建该包装器实例,并在
next.ServeHTTP(wrapped, r)后读取wrapped.statusCode - 注意:如果 handler 没显式调用
WriteHeader,Go 默认用 200,所以初始化时设statusCode: 200 - 不要试图从
Header().Get("Status")读 —— 它只在某些特殊响应头中存在,不可靠
要不要加 req_id?怎么生成才不冲突
加。否则并发请求日志完全混在一起,查问题等于盲人摸象。但别用 uuid.New() 简单生成 —— 它不保证在单次请求内透传,且没考虑 context 取消场景。
实操建议:
- 用
reqID := uuid.NewString()在中间件入口生成,然后通过context.WithValue(r.Context(), key, reqID)注入 context - 后续所有 handler 或子 goroutine 都应从
r.Context().Value(key)取,而不是重新生成 - 避免用全局 map 缓存 req_id → log.Logger 映射 —— 容易泄漏、难清理;slog 的
With()已足够轻量 - 若用第三方日志库(如 zap),确保其
logger.With(...)是 copy-on-write,不会污染其他请求日志
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











