slog.Info("msg", "key", value) 输出 !BADKEY 因参数为奇数,"key"被视作无效键;须用slog.String等构造Attr,配sloglint拦截,并用NewJSONHandler输出真JSON,trace_id需中间件提取后With传递。

为什么 slog.Info("msg", "key", value) 会输出 !BADKEY
因为 slog.Info 接收的是变长 slog.Attr 参数,不是任意 key-value 对。直接传 "key", value 会导致参数个数为奇数,slog 把落单的 "key" 当作无效键,默默转成 !BADKEY 字段。
- 编译不报错、运行不 panic,但线上日志关键字段全变成
!BADKEY,查问题时完全抓瞎 - 正确写法必须用构造函数:用
slog.String("user_id", uid)、slog.Int("attempts", n)显式生成slog.Attr - 配合
golangci-lint+sloglint插件,开启attr-only: true,CI 阶段就能拦截这类错误
如何让 slog 输出真正的 JSON 而不是空格分隔文本
默认 slog.New() 不带 handler,或只传 os.Stdout,它会 fallback 到内部空 handler 或文本格式——看着像结构化,实则是 msg="xxx" user_id=123 这种无法被 Loki/Datadog 自动解析的格式。
- 必须显式用
slog.NewJSONHandler(os.Stdout, nil)包装输出目标,再传给slog.New() - 字段名自动转小驼峰(
user_id→userId),无法关闭;想保留原样得自己实现slog.Handler -
slog.Any("req", req)不会展开 struct,只会调fmt.Sprint;要嵌套 JSON,得用slog.Group("req", slog.String("method", "GET"), ...)
怎么给 HTTP 请求自动注入 trace_id 并透传到每条日志
slog 本身不绑定 context,也没有全局 logger 概念。trace_id 必须手动透传,漏一次就断链。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 别把子 logger 存进
context.Context再取——容易覆盖、漏传、并发不安全 - 推荐在中间件里提取
X-Correlation-ID,用slog.With(slog.String("trace_id", cid))创建子 logger,然后通过参数或局部变量向下传递 - 高频请求下避免每次重复调用
slog.With;可提前构建带固定字段的 logger 实例,如reqLogger := slog.With("service", "api") - 高并发时同 trace_id 日志混在一起难区分,可加
slog.String("goroutine", fmt.Sprintf("%d", runtime.GoID()))
WithGroup 和 With 的区别与误用风险
With 是扁平追加字段,WithGroup 是创建嵌套命名空间。两者语义不同,结构也不同,混用会导致日志平台字段解析失败。
立即学习“go语言免费学习笔记(深入)”;
-
logger.WithGroup("http").Info("req", slog.String("path", "/login"))输出http.path;而With("http.path", "/login")是平级字段,无层级含义 - group 嵌套过深(如
http.auth.token.verify)可能超出日志平台字段名长度限制(Loki 约 200 字符) - goroutine 里用了主线程的 logger 变量,但主线程已调
WithGroup("db"),子 goroutine 日志仍无db.前缀——因为slog.Logger是值类型,复制后修改不影响原实例
"user id")、手动拼 JSON 字符串传给 slog.Info、忘记配置 ReplaceAttr 导致敏感字段泄露——这些都不是边缘情况,而是上线前最容易被跳过的细节。

















