Menu

Golang slog 与 log:Go 结构化日志

如何在 Go 中记录日志:经典的 log 包及其标志和 log.Fatal,以及用于结构化日志的 log/slog(Go 1.21),包括日志级别、键值属性、文本和 JSON 处理器,以及用 With 携带上下文的 logger。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

一个例子看懂 slog

log/slog(Go 1.21)写出结构化的记录:一条消息、一个级别和若干键值属性。

每一行的输出都形如 time=... level=INFO msg="user logged in" user=ada attempts=1。因为每个值都是独立的字段,日志收集系统(Loki、Elasticsearch、CloudWatch、Datadog)可以直接按 user=ada 或 level=ERROR 过滤,不用在自由文本上跑正则表达式。

本页的示例都写到 os.Stdout,这样输出会按顺序显示。在真实的服务中,日志通常写到 os.Stderr,默认 logger 也写到那里。

日志级别

级别值用途
slog.LevelDebug-4给开发者看的细节,生产环境中关闭
slog.LevelInfo0正常事件:已启动、请求已处理、任务已完成
slog.LevelWarn4发生了意外,但程序已经处理
slog.LevelError8某个操作失败了

处理器会丢弃低于其最低级别的记录,而默认的最低级别是 Info。这就是为什么在你配置一个 Level: slog.LevelDebug 的处理器之前,slog.Debug(...) 什么都不会打印。各个值之间留有间隔,可以容纳 slog.Level(2) 这样的自定义级别。

要在运行时修改级别(通过命令行标志、管理接口或信号),就在选项里放一个 slog.LevelVar,之后再对它调用 Set:

var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written

文本还是 JSON

slog.NewTextHandler 写出 key=value 键值对,在终端里很容易阅读。slog.NewJSONHandler 每行写一个 JSON 对象,这是大多数日志管道期望的格式。记录日志的调用保持不变,只需要更换处理器。

值会保留各自的类型:在 JSON 输出中 status 是数字,retry 是布尔值;time.Duration 在文本中打印为 42ms,在 JSON 中打印为纳秒数;error 则打印它的错误信息。ReplaceAttr 是用来改写或删除属性的钩子,这里用它去掉时间戳,实际使用中常用来重命名键(把 msg 改成 message)或隐藏敏感值。

属性

松散类型的写法是键和值交替排列:"user", "ada", "attempts", 3。它很简短,但有一种出错的情况:参数个数是奇数。多出来的那个值会以 !BADKEY 为键记录下来。go vet 能发现这个问题:

./main.go:14:2: call to slog.Info missing a final value

为了类型安全,也为了少一点内存分配,可以使用属性构造函数;在热点路径上记录日志时,使用 LogAttrs:

logger.Info("order placed",
	slog.Int("order_id", 1017),
	slog.String("currency", "EUR"),
	slog.Float64("total", 59.90),
	slog.Duration("took", elapsed),
)

logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))

整个代码库的键要使用同一种命名约定(处处都用 user_id,而不是这个包里用 userID、那个包里用 uid)。日志系统中的查询都依赖于此。

With:携带上下文的 logger

logger.With(attrs...) 返回一个新的 logger,它会把这些属性加到每一条记录上。为每个请求或每个任务创建一个,它写出的每一行就都能关联起来:

每一行都带有 service、version、request_id 和 user,不用在每次调用时重复写。slog.Group 用来嵌套属性,JSON 处理器会把它写成嵌套对象("payment":{"amount":25,"currency":"USD"}),文本处理器则写成带点号的键(payment.amount=25)。logger.WithGroup("db") 会把这个 logger 之后的所有属性都放进一个分组。

把请求作用域的 logger 作为参数或结构体字段往下传。把它存进 context.Context 也可以,但会把依赖关系藏起来;slog 的 InfoContext(ctx, ...) 这类方法会把 context 传给处理器,自定义处理器可以借此取出追踪 ID。

用 LogValuer 隐藏敏感信息

一个类型可以通过实现 slog.LogValuer 来控制自己被记录成什么样子。这样无论是谁记录这个值,密码和令牌都不会进入日志:

User 只记录它的 ID 和邮箱,而单独记录的 Token 会打印成 REDACTED。处理器只在记录真正被写出时才调用 LogValue,所以它也适用于计算代价很高的值。

经典的 log 包

log 比 slog 出现得早,对于小程序和脚本来说仍然够用。它把带日期和时间前缀的行写到标准错误:

标志添加的内容
log.LstdFlags(默认)日期和时间 2009/11/10 23:00:00
log.Lmicroseconds时间精确到微秒
log.LUTC使用 UTC 时间
log.Lshortfile / log.Llongfilemain.go:14 / 完整路径
log.Lmsgprefix把前缀放在消息前面,而不是行首

有三类函数会退出或 panic,它们的区别很重要:

  • log.Fatal、log.Fatalf、log.Fatalln 打印后调用 os.Exit(1)。defer 调用不会执行。 只在 main 中处理启动失败时使用它们,绝不要在库代码或请求处理器中使用。
  • log.Panic 及其同类函数打印后触发 panic,所以 defer 调用会执行,而且这个 panic 可以被恢复。
  • 其他所有函数都只是写一行日志。

要把日志写入文件,就打开文件并把它传给 log.New 或 log.SetOutput;io.MultiWriter(os.Stderr, f) 可以同时写到两处。

同时使用 log 和 slog

slog.SetDefault(logger) 会把 logger 设为顶层 slog.Info 等函数的默认 logger,同时也让 log 包的输出经过它。这样,你的代码或依赖中已有的 log.Printf 调用,都会以 Info 级别的结构化记录输出:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}

在调用 SetDefault 之前,默认的 slog logger 是通过 log 包输出的,这就是为什么单独一句 slog.Info("hi") 会打印 2026/09/23 14:30:00 INFO hi。

实用规则

  • 错误要么记录,要么返回,不要两样都做。 一个函数如果既记录错误又返回它,同一个失败就会在调用栈的每一层都被记录一次。把错误连同上下文向上返回,在处理它的地方记录一次即可。
  • 把变化的数据放进属性,而不是消息里。 logger.Info("user created", "user_id", id) 在日志系统中很好归类;logger.Info(fmt.Sprintf("user %d created", id)) 则会为每个用户生成一条不同的消息。
  • 绝不要记录敏感信息或完整的请求体。 用 LogValuer 或 ReplaceAttr 隐藏它们。
  • 生产环境用 JSON,开发环境用文本。 在启动时根据命令行标志或环境变量选择处理器。
  • 慎重选择日志级别。 如果所有内容都以 Error 级别记录,针对错误的告警就会变成噪音。

常见问题

Go 里的 slog 是什么?

log/slog 是 Go 1.21 加入标准库的结构化日志包。每条记录不再是格式化的字符串,而是包含一条消息、一个级别(Debug、Info、Warn、Error)和若干键值属性,再由处理器把它写成 key=value 文本或 JSON:slog.Info("login", "user", "ada", "attempts", 3)。

slog 怎么开启 debug 日志?

默认的最低级别是 Info,所以 slog.Debug 什么都不会打印。创建一个级别更低的处理器,并把它设为默认:slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))。如果想在程序运行时修改级别,就用 slog.LevelVar 代替常量。

Go 中 log 和 slog 有什么区别?

log 写的是自由格式的行,可以加时间戳前缀,没有级别。slog 写的是带级别和带类型键值属性的记录,日志收集系统可以解析和过滤它们。两者都在标准库中;slog.SetDefault 还会把 log 包的输出也转交给 slog 处理器。

log.Fatal 会执行 defer 的函数吗?

不会。log.Fatal 和 log.Fatalf 打印消息后调用 os.Exit(1),这会跳过所有 defer 调用。只在 main 或没有需要清理的初始化代码中使用它们。log.Panic 则会触发 panic,所以 defer 调用会执行。

Coddy 编程语言插图

和 Coddy 一起学编程

开始