slog.NewJSONHandler输出空对象或乱码,根本原因是未正确构造结构化字段(须用slog.String等函数而非裸键值对)、错误嵌套slog.With与slog.Group、或context传入不当;正确做法是独立传slog.Group为日志参数、用slog.With附加上下文字段,并确保Go版本≥1.22。

Go 1.21+ 的 slog 默认不输出结构化字段(比如 JSON),直接用会看到一堆未解析的键值对——这不是 bug,是设计选择:它把序列化逻辑交给你控制。
为什么 slog.NewJSONHandler 输出的是空对象或乱码?
常见现象:日志看起来像 {"time":"...","level":"INFO","msg":"hello"},但自定义字段全丢了;或者字段名变成 ""、"k"、"v" 这种奇怪键名。
根本原因是没正确传入 context.Context 或用了错误的 slog.Group 套嵌方式,导致字段被丢弃或扁平化失败。
- 确保 handler 是用
slog.NewJSONHandler(os.Stdout, nil)创建,而不是slog.NewTextHandler - 所有结构化字段必须通过
slog.String("key", "val")、slog.Int("count", 42)等构造函数传入,不能直接写"key", "val"(那是老式log.Printf风格) - 避免在
slog.With()后再调用slog.With().Info()多次嵌套Group,容易触发字段覆盖或丢失
slog.With() 和 slog.Group() 的区别在哪?
slog.With() 是“附加字段到后续所有日志”,适合请求 ID、用户 ID 这类上下文信息;slog.Group() 是“把字段打包成一个嵌套对象”,用于建模结构化数据(如 HTTP 请求体、数据库查询参数)。
错误用法:slog.With(slog.Group("req", slog.String("method", "GET"))).Info("start") —— 这会让 req 变成顶层字段,且内部字段无法被 JSON handler 正确识别。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确分组写法:
slog.Info("request", slog.Group("req", slog.String("method", "GET"), slog.String("path", "/api"))) - 正确上下文写法:
logger := slog.With(slog.String("trace_id", traceID)); logger.Info("db query", slog.String("sql", "SELECT * FROM users")) - 混合使用时,
Group必须作为独立参数传给日志方法,不能塞进With()
如何让 slog 兼容现有 log 接口或第三方库?
很多中间件(如 chi、gin)只接受 log.Logger,而 slog.Logger 不兼容。硬转不可行,得靠适配器。
Go 官方没提供转换工具,但可以用 slog.Handler + log.SetOutput 做轻量桥接:
var stdLog = log.New(slog.NewTextHandler(os.Stderr, nil), "", 0) log.SetOutput(stdLog.Writer())
注意:这只会把标准库 log.Printf 日志转为文本格式,不带结构化字段;如果要保留结构,需自己实现 io.Writer 将 slog.Record 序列化后写入。
- 第三方框架集成优先查其文档是否已支持
slog.Handler(如echov5 支持echo.WithLogger(slog.New(...))) - 不要试图用
func() { ... }()匿名函数包装slog来模拟log.Logger,字段会丢失 - 测试中想捕获结构化输出?用
bytes.Buffer+slog.NewJSONHandler,再手动解析 JSON 字段验证
真正难的不是怎么打结构化日志,而是字段命名一致性、敏感字段过滤(比如 password)、以及和 tracing 系统(如 OpenTelemetry)的 context 透传——这些 slog 都不处理,得你自己在 Handler 层拦截 Record 做加工。










