slog.With 返回新 Logger 并冻结键值对到其 handler 上下文,后续日志自动携带;链式调用同名键会覆盖,需显式合并;中间件应注入子 logger 到 context 而非修改全局变量;WithGroup 与 With 顺序影响嵌套结构,group 名禁用点号或斜杠。

为什么 With 不是“每次打日志都调用一次”
slog.With 返回的是一个新 slog.Logger,不是修改原 logger。它把传入的键值对“冻结”进新 logger 的 handler 上下文中,后续所有通过该 logger 发出的日志都会自动带上这些字段。误以为它是临时附加、每次都要重写,会导致重复嵌套、字段爆炸或性能浪费。
- 错误写法:
logger.With("user_id", uid).Info("login success"); logger.With("user_id", uid).Warn("rate limit")—— 创建了两个独立 logger,但没复用 - 正确思路:按业务边界提前构造带上下文的 logger,比如
userLogger := logger.With("user_id", uid),之后所有userLogger.Info都含user_id - 注意:
With本身不触发日志输出,只是组合;真正开销在最终Info/Error调用时,由 handler 决定是否序列化字段
With 的键名冲突与覆盖规则
多个 With 链式调用时,后出现的同名键会覆盖先出现的。这不是 bug,是设计行为,但容易被忽略导致关键字段被意外替换。
- 例如:
l1 := base.With("trace_id", "a"); l2 := l1.With("trace_id", "b")→l2输出日志中trace_id值为"b" - 如果想避免覆盖(比如希望保留原始 trace_id + 新增 span_id),必须显式合并:
base.With("trace_id", tid).With("span_id", sid),不能依赖“继承” - handler 实现(如
slog.JSONHandler)不校验键名合法性,传入nil或空字符串作为键名会导致 JSON key 为null或空字符串,解析端可能崩溃
在 HTTP 中间件里用 With 绑定请求级字段的典型模式
Web 服务最常见场景:给每个请求生成唯一 request_id,并让其贯穿整个处理链路。直接在 handler 里 logger.With("request_id", rid).Info(...) 不够干净,中间件 + context 是更可控的方式。
- 推荐做法:中间件中创建子 logger 并注入到
context.Context:ctx = context.WithValue(r.Context(), loggerKey, logger.With("request_id", rid)) - 后续 handler 从 ctx 取 logger:
logger := ctx.Value(loggerKey).(slog.Logger),直接使用,无需再With - 切忌在中间件里直接
logger = logger.With(...)并赋值给包级变量 —— 会污染全局 logger,引发并发写 panic 或字段混杂 - 注意:Go 1.21+ 的
slog.With支持任意数量键值对,但必须成对出现;传奇数个参数(如With("a"))会在运行时报invalid number of arguments to With
With 和 WithGroup 混用时的字段嵌套陷阱
当需要组织逻辑分组(如数据库操作、外部 API 调用)时,WithGroup 很有用,但它和 With 的嵌套顺序会影响最终 JSON 结构,尤其对接结构化日志系统(如 Loki、Datadog)时字段路径必须稳定。
-
logger.With("service", "api").WithGroup("db").Info("query")→ 输出中service在顶层,db下无字段(因为WithGroup后没追加任何键值) - 要让字段进入 group,必须在
WithGroup后再With:logger.WithGroup("db").With("sql", stmt).Info("exec")→{"db":{"sql":"SELECT..."}} - 反向操作(
WithGroup套在With外)无效:logger.WithGroup("outer").With("k","v")中的k仍在顶层,不进outer - group 名不支持点号(
.)或斜杠(/),否则某些 handler(如JSONHandler)会静默丢弃该 group
slog.NewTextHandler(os.Stdout, nil) 打印调试,比猜更可靠。

















