Golang log/slog 使用教程:结构化日志实战指南
Go 语言的日志记录功能经历了漫长的发展。过去,开发者依赖简单的标准 log 包或功能强大的第三方库(如 zap 和 zerolog)。随着 Go 1.21 中 log/slog 包的引入,Go 语言 2025-11-19 10:6:20 Author: blog.axiaoxin.com(查看原文) 阅读量:49 收藏

Go 语言的日志记录功能经历了漫长的发展。过去,开发者依赖简单的标准 log 包或功能强大的第三方库(如 zap 和 zerolog)。随着 Go 1.21 中 log/slog 包的引入,Go 语言现在拥有了一个原生的、高性能的、结构化日志解决方案,旨在成为新的标准。

结构化日志 (Structured Logging) 的核心在于使用键值对(Key-Value Pairs)来记录日志,这使得日志可以被机器解析、过滤、搜索和可靠地分析。这对于观察系统的详细行为和调试问题至关重要。

slog 的设计将日志逻辑与其最终输出分离开来,提供了一个通用的 API,同时允许不同的日志实现来控制输出格式和目的地。

slog 的核心组件

log/slog 包围绕三个核心类型构建:Logger、Handler 和 Record。

1. Logger (日志记录器)

  • Logger 是日志创建的入口点(API),提供面向用户的方法,如 Info()、Debug() 和 Error()。
  • 当调用这些方法时,Logger 会创建一个 Record,然后将其传递给配置好的 Handler 进行处理。
  • 顶层函数(如 slog.Info)调用默认 Logger 的对应方法。

2. Handler (处理程序)

  • Handler 是一个接口,负责处理 Record。它是决定日志 如何 和 写入何处 的引擎。
  • Handler 负责将 Record 格式化为特定的输出(如 JSON 或纯文本)并写入目标(如控制台或文件)。
  • log/slog 包含内置的 TextHandler(格式化为 key=value 键值对)和 JSONHandler(格式化为 JSON)实现。
  • Handler 接口的设计使其具有高度的可组合性,可以创建包裹其他 Handler 的“中间件”来丰富、过滤或修改日志记录。

3. Record (日志记录)

  • Record 代表一个独立的日志事件。
  • 它包含事件的所有必要信息:事件时间、严重级别(INFO, WARN 等)、日志消息以及所有结构化的键值属性。
  • Record 是日志条目在被格式化之前的原始数据。

slog 的基础使用与配置

1. 使用默认 Logger

slog 提供了顶层函数来使用默认 Logger,初始默认 Logger 的输出格式与旧 log 包相似,但会包含日志级别信息。

package main

import "log/slog"

func main() {
    slog.Info("hello, world")
    // 默认输出类似: 2023/08/04 16:09:19 INFO hello, world
}

要显式获取默认 Logger,可以使用 slog.Default()。

2. 创建和定制 Logger

通常,你会通过 slog.New() 函数创建一个自定义 Logger,并为其指定一个 Handler。

JSON Handler (生产环境推荐):

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user logged in", "user_id", 123)
/* 输出示例:
{ "time" : "..." , "level" : "INFO" , "msg" : "user logged in" , "user_id" : 123 }
*/

Text Handler (Logfmt 格式):

logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("database connected", "db_host", "localhost", "port", 5432)
/* 输出示例:
time=... level=INFO msg="database connected" db_host=localhost port=5432
*/

3. 设置默认 Logger

使用 slog.SetDefault() 可以替换包级别的默认 Logger。这样做会使顶层函数(如 slog.Info)使用新的配置。

更重要的是,SetDefault() 还会更新 log 包的默认 Logger,从而使现有使用 log.Printf 的应用程序也能无缝切换到结构化日志输出。

slog 日志级别控制

1. 内置级别

slog 提供了四个默认严重性级别,它们都是整数值:

  • slog.LevelDebug (-4)
  • slog.LevelInfo (0)
  • slog.LevelWarn (4)
  • slog.LevelError (8)

默认情况下,所有 Logger 都配置为记录 slog.LevelInfo 及更高级别的消息,这意味着 DEBUG 消息会被抑制。级别之间的差距(4)是故意的,为自定义级别留出了空间。

2. 设置最低级别

通过 slog.HandlerOptions 可以控制将被处理的最低级别:

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug, // 启用 DEBUG 级别日志
})
logger := slog.New(handler)

3. 动态更新日志级别

如果需要在生产环境中不重启服务的情况下更改日志详细程度,可以使用 slog.LevelVar 类型。这是一个动态的日志级别容器,可以通过其 Set() 方法随时并发安全地更新级别。

var logLevel slog.LevelVar // 默认是 INFO
// ...
// 构造 Logger 时传入 LevelVar 的指针
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: &logLevel,
}))

// 随时更新级别
logLevel.Set(slog.LevelDebug)

4. 自定义级别

你可以定义新的常量来创建自定义日志级别,例如 TRACE (-8) 或 FATAL (12):

const (
    LevelTrace = slog.Level(-8)
    LevelFatal = slog.Level(12)
)

使用自定义级别时,必须使用通用的 logger.Log() 或 logger.LogAttrs() 方法,并显式指定级别:

logger.Log(context.Background(), LevelFatal, "database connection lost")

若要修复默认输出中显示的级别名称(例如 ERROR+4),可以使用 HandlerOptions 中的 ReplaceAttr() 函数将级别整数值映射到自定义字符串。

5. 性能优化(避免昂贵操作)

在记录日志之前,如果需要进行昂贵的计算来准备数据,应该先检查 Logger 是否启用了该级别,以防止不必要的性能损失:

if logger.Enabled(context.Background(), slog.LevelDebug) {
    // 只有在 DEBUG 级别启用时才执行昂贵操作
    logger.Debug("operation complete", "data", getExpensiveDebugData())
}

slog 结构化属性(Attrs)

结构化日志的优势在于通过键值对(Attr)来丰富日志条目,使其可查询。

1. 键值对风格 (Key-Value Pairs)

最方便的方式是传递交替的键和值序列:

logger.Info("incoming request", "method", "GET", "status", 200)

⚠️ 风险警告: 这种便利的键值对风格存在重大隐患。如果参数数量为奇数(缺少值),slog 不会 panic 或返回错误,而是静默创建一个带有特殊键 !BADKEY 的损坏日志条目。

2. 强类型 Attr(推荐)

为了保证日志的正确性和可靠性,强烈推荐使用强类型的 slog.Attr 辅助函数。这可以在编译时捕获错误,防止产生不平衡的键值对:

logger.Warn(
    "permission denied",
    slog.Int("user_id", 12345),
    slog.String("resource", "/api/admin"),
)

slog 提供了多种构造函数来创建 Attr,如 slog.String、slog.Int、slog.Bool、slog.Time 和用于任意类型的 slog.Any。

3. LogAttrs:最高效的方式

对于频繁执行的日志语句,使用 logger.LogAttrs 方法是最有效的方式。它只接受 slog.Attr 类型的参数,从而避免了内存分配,性能更高。

// LogAttrs 仅接受 Attr 类型,效率更高
logger.LogAttrs(
    context.Background(),
    slog.LevelInfo,
    "hello, world",
    slog.String("user", os.Getenv("USER")),
)

4. 日志分组(Groups)

你可以使用 slog.Group() 将多个属性收集到一个命名组下,以增加日志的结构性并避免键冲突。

  • JSONHandler 将组显示为嵌套的 JSON 对象。
  • TextHandler 将组名作为前缀,用点号分隔(例如 properties.width=4000)。

Go 1.25.0 增加了更高效的 slog.GroupAttrs(),它只接受 Attr 列表。

logger.Info(
    "image uploaded",
    slog.Int("id", 23123),
    slog.Group("properties",
        slog.Int("width", 4000),
        slog.Int("height", 3000),
    ),
)

5. 子 Logger 和属性继承

使用 logger.With() 方法可以创建一个新的 Logger,该 Logger 继承父 Logger 的所有属性,并添加新的属性。这些公共属性只会被格式化一次(在调用 With 时),这对于性能优化非常有益。

你也可以使用 logger.WithGroup(name) 创建一个子 Logger,该子 Logger 的所有后续属性(包括在日志调用点添加的属性)都将以该组名限定。这有助于在大型系统中避免命名冲突。

slog 自定义 Handler

Handler 是 slog 灵活性的关键所在。

1. HandlerOptions 配置

内置的 TextHandler 和 JSONHandler 可以通过 slog.HandlerOptions 进行配置:

  • Level: 设置最低日志级别。
  • AddSource: 设置为 true 会自动包含日志语句的源代码文件、函数和行号信息。但请注意,这会带来性能开销,因为它需要调用 runtime.Caller()。
  • ReplaceAttr: 这是一个函数,用于在日志记录之前重写每个非分组属性。它可以用于更改内置属性(如时间和级别)的键名,转换类型,或删除敏感信息。ReplaceAttr 会为每个日志记录中的每个属性调用一次,因此其逻辑应尽可能快。

2. 写入文件

最佳实践通常是记录到 stdout 或 stderr,并让运行时环境管理日志流。如果需要直接写入文件,可以将 *os.File 实例传递给 Handler。对于日志文件轮换,可以使用标准的 logrotate 工具或 lumberjack 包。

3. 自定义和第三方 Handler

Handler 接口定义了四个方法,负责处理日志记录的生命周期:

type Handler interface {
    // Enabled 报告是否处理给定级别的记录,用于快速丢弃不必要的日志事件。
    Enabled(context.Context, Level) bool
    // Handle 处理实际的日志记录,仅在 Enabled 返回 true 时调用。
    Handle(context.Context, Record) error
    // WithAttrs 返回一个新的 Handler,其属性包含接收者属性和传入的属性。
    WithAttrs(attrs []Attr) Handler
    // WithGroup 返回一个新的 Handler,将后续属性限定在给定名称的分组下。
    WithGroup(name string) Handler
}

由于 Handler 是一个接口,你可以实现自定义 Handler 来实现不同的格式化或输出目的地。社区也提供了许多有用的第三方 Handler:

  • slog-sampling: 用于丢弃重复日志条目,实现日志采样。
  • tint: 用于在开发环境中将日志输出彩色化。
  • slog-multi: 用于高级组合模式,例如扇出、缓冲、条件路由等。

处理重复键: 内置 Handler 不会自动对日志中的重复键进行去重,这可能导致观测工具行为不确定。如果需要去重,你需要使用第三方“中间件” Handler,例如 slog-dedup。

4. 集成第三方日志处理器

slog 的设计目标之一是提供一个统一的 API ,允许开发者在不修改核心日志代码的情况下切换高性能的 Handler。这使得 slog 可以与高性能库(如 Zap 或 Zerolog)结合使用,以获得高性能和标准 API 的双重优势。

slog 高级日志模式

1. 上下文日志(Contextual Logging)

slog 提供了一组带有 context.Context 参数的方法(如 InfoContext()),这些方法允许 Handler 提取上下文中的信息,例如追踪 ID (Trace ID)。

重要说明: 内置的 slog Handler 不会自动从 context.Context 中拉取值。你必须使用上下文感知 Handler(如社区的 slog-context 包)才能实现上下文属性的传播。

使用全局 Logger 和上下文 Handler (推荐做法之一): 这种模式使用全局 Logger 并配置一个上下文感知的 Handler。请求处理中间件将上下文属性(如 correlation_id)附加到 context.Context 中(例如使用 slogctx.Prepend())。后续的日志调用(如 slog.InfoContext(r.Context(), ...))将上下文传递给全局 Logger,Handler 则从 Context 中提取并写入这些属性。

不推荐的做法: 尽管某些第三方库允许将 Logger 实例本身放入 context.Context 中,但 Go 团队最终从 slog API 中删除了相关辅助函数(如 slog.NewContext()),因为它被认为是一种隐式的依赖关系,使代码难以理解和测试。

依赖注入模式: 另一种推荐模式是将 Logger 视为正式的依赖项,通过结构体字段或函数参数显式传递,这使得代码更具可测试性和灵活性。

2. LogValuer 接口:控制类型输出

通过实现 slog.LogValuer 接口,你可以精确控制自定义类型在日志中的显示方式。

  • 隐藏敏感数据: 你可以利用此接口来隐藏结构体中的敏感字段(如密码哈希),确保只有安全的信息被记录,例如只记录用户 ID。
  • 延迟计算: 可以将昂贵的计算操作封装在 LogValuer 中,确保只有在日志级别启用时,LogValue() 方法才会被调用,从而避免不必要的性能开销。
type Token string
// LogValue 实现 slog.LogValuer 接口,避免泄露秘密
func (Token) LogValue() slog.Value {
    return slog.StringValue("REDACTED_TOKEN")
}
// 记录时,Token 值将被替换为 "REDACTED_TOKEN"

3. 错误日志记录

在 slog 中记录错误时,应使用 slog.Any() 来包含错误值。

err := errors.New("payment gateway unreachable")
logger.Error("Payment processing failed", slog.Any("error", err))

如果使用自定义错误类型,可以实现 LogValuer 接口来提供结构化的错误信息(如错误码和原因),这对生产系统的分析非常有价值。

堆栈跟踪 (Stack Trace) 捕获: slog 没有内置捕获堆栈跟踪的功能。你需要集成第三方包(如 go-xerrors)并使用 HandlerOptions 中的 ReplaceAttr() 函数,来提取、格式化和添加堆栈跟踪信息到日志记录中。

slog 性能考量与最佳实践

性能概述

尽管 slog 的设计考虑了性能,但在某些基准测试中,它仍然比高度优化的第三方库(如 zerolog 和 zap)慢。这种性能差异是设计权衡的结果,Go 团队的优化集中在最常见的日志模式(超过 95% 的调用属性少于 5 个)。

性能优化策略:

  • 预格式化属性: 使用 Logger.With 添加通用属性,Handler 只会格式化一次,从而提高速度。
  • 避免反射和分配: 尽可能使用 logger.LogAttrs,因为它只接受 Attr 类型,可以最小化内存分配,实现最高效的日志输出。
  • 延迟计算: 使用 logger.Enabled() 或 LogValuer 接口,确保昂贵的计算操作仅在日志级别启用时才执行。

可观测性与日志处理

  1. 中央化日志系统: 将结构化日志从服务器发送到中央可观测性平台(如 Dash0)至关重要。
  2. 日志关联性: 为了实现真正的可观测性,日志条目必须与分布式追踪等其他遥测信号共享公共上下文(如 TraceID)。这可以通过集成 OpenTelemetry 的 otelslog bridge 来实现。
  3. 日志规范化: 实施 LogValuer 接口以标准化应用程序中自定义类型的日志表示,并确保敏感数据被省略。
  4. Linting 规范: 由于 slog 允许混合键值对和 slog.Attr 两种风格,为保证代码库一致性,应使用 sloglint 等 Linter 工具来强制执行日志风格规则(例如,强制只使用 Attr)。
  5. 日志采样: 在高流量环境中,使用日志采样(如 slog-sampling)仅记录具有代表性的子集,以控制数据摄取成本。
  6. 本地持久化: 最佳实践通常是将日志输出到 stdout/stderr,让运行时环境(如 Docker 或 Systemd)或专用的日志转发器(如 Vector, Fluentd)负责收集和持久化。将日志先写入本地文件可以提供备份和缓冲,以防集中式日志管理系统出现问题。

总结: log/slog 是 Go 生态系统的一个重要里程碑,它提供了一个强大的基础,支持开箱即用地构建高度可观测的系统。通过理解 Logger、Handler 和 Record 的核心概念,并遵循结构化日志的最佳实践,你可以将服务从不透明的黑盒转变为透明、易于诊断和故障排除的系统。


文章来源: https://blog.axiaoxin.com/post/go-slog-usage/
如有侵权请联系:admin#unsafe.sh