必须用 slog.New(slog.NewJSONHandler(os.Stdout, nil)) 才能输出真正 JSON;字段名强制小驼峰且不可自定义;slog.Any 不展开 struct,需用 slog.Group;key-value 必须用 slog.String 等构造 Attr,避免 !BADKEY;HTTP 日志需中间件注入 trace_id。

怎么让 slog 输出真正的 JSON 而不是纯文本
默认调用 slog.Info 会走文本格式,哪怕你写了 slog.NewJSONHandler,但没把它真正“装进” slog.New,就还是老样子——看着像结构化,实则只是空格分隔的 key=value。
- 必须显式用
slog.NewJSONHandler(os.Stdout, nil)包一层输出目标,再传给slog.New,缺这步就白搭 - 字段名强制小驼峰(如
user_id→userId),无法自定义;想保留下划线就得自己写 handler 或换库 -
slog.Any("req", req)不会自动展开 struct 字段,只会调fmt.Sprint转成字符串——要嵌套 JSON,得手动用slog.Group
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("login", slog.String("user_id", "u123"), slog.Int("attempts", 2))
为什么 key, value 写法会悄悄埋雷
直接写 slog.Info("msg", "user_id", 123) 看似省事,但 slog 会把非偶数个参数里“落单”的 "user_id" 当作无效 key,输出成 !BADKEY 字段。线上查日志时发现关键字段全是 !BADKEY,基本就是这个原因。
- 编译器不报错,运行时也不 panic,只默默污染日志字段名
- 正确写法是统一用
slog.String、slog.Int等构造slog.Attr,类型和 key 名都在编译期校验 - 配合
golangci-lint+sloglint插件,开attr-only: true,能直接在 CI 拦住这类写法
HTTP 请求怎么自动带 trace_id 到每条日志
微服务里没有 trace ID 的日志等于没日志。slog 本身不提供上下文透传,但靠 slog.With + context + 第三方 slog-context 可以低成本实现。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 中间件里生成或提取
X-Correlation-ID,用slog.With(slog.String("trace_id", cid))创建子 logger - 别把子 logger 存到
context里再取——容易漏传或覆盖;更稳的做法是封装一个RequestLogger(r *http.Request)函数,每次按需造 - 注意:slog 默认不记录 goroutine ID,高并发下同 trace_id 日志混在一起时,可能需要额外加
slog.String("goroutine", fmt.Sprintf("%d", runtime.GoID()))
结构化日志字段命名和类型有哪些硬约束
字段不是随便起名、随便传值就能被日志系统友好解析的。slog 对字段名和类型有隐性要求,踩中就会丢数据或解析失败。
- 字段名不能含空格、点号、斜杠等特殊字符,否则 JSON handler 会静默跳过该 attr
- 数值字段必须用
slog.Int/slog.Float64,传字符串再转数字?日志里存的就是字符串,Loki 或 ES 无法做 range 查询 - 敏感字段如
password、token必须脱敏,slog.String("token", "***")是底线,别图快直接打原始值 - 时间字段优先用
slog.Time("created_at", time.Now()),而不是字符串拼接,避免时区/格式不一致
最常被忽略的是:slog 不支持嵌套结构体自动序列化,slog.Any("user", userStruct) 输出的是 {"user":"{...}"},不是展开的 object——真要嵌套,得用 slog.Group("user", ...) 手动摊平。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










