不能直接用 log.printf 打链路日志,因其缺乏上下文透传能力,无法自动携带 traceid、请求开始时间等关键字段,且手动拼接时间戳会破坏结构化日志;应使用 context.context 与 slog 结合,在入口初始化带 traceid 和 start_time 的 ctx,并通过 slog.withattrs 预置字段,配合 replaceattr 屏蔽默认 time 以确保时间戳唯一且不重复生成,同时需显式传递 logger 而非仅 context 到 goroutine 中。

为什么不能直接用 log.Printf 打链路日志
因为缺乏上下文透传能力,log.Printf 输出的日志无法自动携带请求 ID、时间戳、调用层级等关键字段,同一请求分散在多个 goroutine 或微服务中时,日志完全无法关联。更麻烦的是,手动拼接时间戳(比如 time.Now().Format(...))会破坏日志结构化,也容易因格式不统一导致下游解析失败。
用 context.Context + log/slog 搭建带时间戳的链路日志骨架
Go 1.21+ 原生 slog 支持 context 绑定,配合自定义 slog.Handler 可自动注入时间戳与 traceID。关键不是“加时间戳”,而是让时间戳随 context 流动、且不重复生成。
- 在入口处(如 HTTP handler)创建带 traceID 和初始时间的 context:
ctx := context.WithValue(r.Context(), "trace_id", uuid.New().String()) ctx = context.WithValue(ctx, "start_time", time.Now())
- 用
slog.WithAttrs将 traceID 和时间戳预置进 logger:logger := slog.With( slog.String("trace_id", ctx.Value("trace_id").(string)), slog.Time("ts", ctx.Value("start_time").(time.Time)), ) - 后续所有
logger.Info调用都会自动带上这两个字段,且ts是请求开始时间,不是每条日志的当前时间——这对耗时分析更真实
slog.Handler 中如何避免时间戳被覆盖或重复格式化
如果直接在 Handle 方法里调用 time.Now(),每个日志行都会打上不同时间戳,失去“请求粒度”意义;若用 slog.Time 预置又可能被后续 With 覆盖。稳妥做法是只在 context 初始化时生成一次时间戳,并通过 HandlerOptions.ReplaceAttr 过滤掉冗余时间字段:
- 禁止 handler 自己生成时间:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == "time" { // 屏蔽 slog 默认插入的 time 字段 return slog.Attr{} } return a }, }) - 确保所有日志都显式携带
ts字段,且该字段只来自 context 初始化值,不依赖 handler 行为 - 注意:Go 1.22+ 的
slog.WithGroup会改变 attr 作用域,若用 group 包裹日志,需确认ts是否仍处于顶层 scope
跨 goroutine 传递链路日志上下文的实际陷阱
Go 的 context 不会自动跨 goroutine 传播,go fn(ctx) 写法看似传了,但若 fn 内部没用 ctx 构造新 logger,日志就还是无上下文的默认实例。
- 必须显式把 logger 作为参数传入并发函数:
go func(ctx context.Context, logger *slog.Logger) { logger.Info("in goroutine", "step", "cache_fetch") }(ctx, logger) - 切忌在 goroutine 内重新调用
slog.With(...)—— 这会丢失原始 context 中的ts和trace_id,除非你手动从 context 取出再塞进去 - 数据库查询、RPC 调用等异步操作,建议封装成接受
context.Context和*slog.Logger的函数,而不是只传 context
时间戳本身不是难点,难的是让它和 traceID 一起,在任意深度的调用栈和 goroutine 中保持一致且不污染日志结构。漏掉一次 context 传递,整条链路就断了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











