slog.JSONHandler 自 Go 1.22 起稳定可用,Go 1.21 需启用 GOEXPERIMENT;zap 需手动构建 JSON Core 而非 NewProduction;logrus 必须先 SetOutput 再 SetFormatter;error 和 context 需显式序列化,字段须导出并加 JSON tag。

log/slog 默认不支持 JSON 输出,必须自定义 Handler
slog 是 Go 1.21+ 官方推荐的日志包,但它内置的 slog.TextHandler 和 slog.JSONHandler 并非都开箱即用——slog.JSONHandler 确实存在,但只在 Go 1.22+ 中才正式稳定(Go 1.21 中是实验性功能,需启用 GOEXPERIMENT=slog)。很多用户升级后仍输出纯文本,就是因为没确认 Go 版本或没正确构造 JSONHandler。
正确做法是:确保 Go ≥ 1.22,然后直接传入 os.Stdout 和可选的 slog.HandlerOptions:
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}))
- Go 1.21 用户若强行使用
slog.JSONHandler,会遇到undefined: slog.JSONHandler或运行时 panic -
AddSource: true会添加文件名和行号,但会略微降低性能,生产环境建议关掉 - 不指定
Level时默认为LevelInfo,低于该级别的日志(如Debug)会被静默丢弃
用 zap 替代 slog 时,避免误用 zap.NewProduction() 直接输出 JSON
zap 是最常用的结构化日志库,但新手常以为 zap.NewProduction() 就是“JSON 模式”——其实它输出的是预格式化的 JSON 字符串,字段名固定(如 "level"、"ts"),且无法自由增减字段。真正需要灵活控制 JSON 结构(比如加 trace_id、统一 service_name)时,得用 zap.New(zapcore.NewCore(...)) 手动组装。
关键点在于选择正确的 Encoder:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.TimeKey = "timestamp"
encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderCfg),
zapcore.Lock(os.Stdout),
zapcore.DebugLevel,
)
logger := zap.New(core)
- 别直接用
zapcore.NewConsoleEncoder,它输出的是人类可读格式,不是 JSON -
encoderCfg.EncodeTime必须显式设置,否则默认用浮点秒,不符合多数日志平台对 ISO 时间的要求 - 如果写入文件,记得用
zapcore.AddSync包裹*os.File,否则可能丢失日志
logrus 设置 JSON 输出时,SetFormatter 必须在 SetOutput 之后调用
logrus 的配置顺序敏感:如果先调用 logrus.SetFormatter(&logrus.JSONFormatter{}) 再调用 logrus.SetOutput(...),部分版本(尤其是 v1.9+)会出现空输出或 panic。这是因为 formatter 初始化时会尝试访问 output 的属性,而此时 output 还是 nil。
安全写法是严格按顺序:
log := logrus.New()
log.SetOutput(os.Stdout) // 第一步:设输出目标
log.SetFormatter(&logrus.JSONFormatter{ // 第二步:设格式器
TimestampFormat: time.RFC3339Nano,
DisableHTMLEscape: true,
})
log.SetLevel(logrus.DebugLevel)
-
DisableHTMLEscape: true很重要,否则日志里出现<script>会被转义成<script></script>,破坏原始数据语义 - 不要用
logrus.WithFields在每条日志前重复构造 map,应复用log.Entry实例减少分配 - v2 版本已改名
github.com/sirupsen/logrus,导入路径写错会导致构建失败
自定义 JSON Handler 时,注意 context.Context 和 error 的序列化陷阱
标准库 slog 和第三方库(如 zap)对 error 类型和 context.Context 的处理方式不同:默认不会递归展开 error 的 cause 链,也不会提取 context 中的 value。想让 err 字段包含完整堆栈或链式错误信息,必须显式转换。
例如用 slog 记录带堆栈的 error:
import "runtime/debug"
logger.Error("db query failed",
"err", slog.StringValue(fmt.Sprintf("%+v\n%s", err, debug.Stack())),
"query", query,
)
- 直接传
err(即"err", err)只会调用err.Error(),丢失堆栈和 wrapped error -
context.Context不能直接作为日志值传入,需提前解包:"trace_id", ctx.Value("trace_id") - 所有自定义 struct 若想被 JSON handler 正确序列化,必须导出字段(首字母大写)且有对应 JSON tag,否则字段为空

















