登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go slog 怎样让字段按模块分组

来源:17golang原创

时间:2026-10-21 21:46:04 138浏览 收藏

Go slog 要让字段按模块分组,我会先区分两件事:一条日志中的局部对象用 slog.Group,一个模块持续输出的字段用 Logger.WithGroup 派生子 Logger。服务名、任务标识等公共字段先用 With 绑定,再进入模块组;这样既有模块命名空间,也能保留清楚的公共入口。

我更在意的是查询者能否判断字段归属,而不是日志看起来有没有层次。下面以一个虚构的商品目录导入任务为例讨论我的设计取舍,代码与数值都是教学示例,不是线上事故复盘或性能实测。log/slog 自 Go 1.21 进入标准库,背景见 https://go.dev/blog/slog。

官方文档:https://pkg.go.dev/log/slog

我会先决定日志要让谁读

同一个 rows,可能表示输入文件行数,也可能表示实际写入行数。如果全部塞进根部,再靠消息文字猜语义,字段名就容易失去稳定含义。我会把“跨模块关联”和“模块内部解释”分开:service、task_id 在根部,导入模块的 mode、rows 放进 import,来源描述放在它的 source 内。

这是本例的字段约定,不是 slog 强制的业务模型。若团队已经用固定的顶层 module 标签做筛选,也可以保留该标签,再决定是否需要嵌套对象。我的判断标准是字段消费者,而不是目录结构有几层。

单条记录的局部字段交给Group

slog.Group 创建一个分组属性,只包含传给它的字段。以下片段假定已有完整示例中的 task;decision 是 input 的同级属性,不会因为写在它后面就自动进入组。

// 接在已经定义的 task Logger 后:只把 rows 和 format 包进 input。
task.Info("file described",
	slog.Group("input",
		slog.Int("rows", 24),
		slog.String("format", "csv"),
	),
	slog.String("decision", "preview"),
)

在内置 JSONHandler 中,input 对应嵌套对象;TextHandler 使用带点的限定键名。第三方 Handler 可以有自己的表现方式,不能把文本外观直接当成所有后端的存储协议。官方分组说明:https://pkg.go.dev/log/slog#hdr-Groups。

对我来说,这种局部分组适合文件摘要、请求描述等成套字段:只在需要时出现,不把整个模块的 Logger 改成“当前文件对象”。

task记录中input分组与同级decision字段的静态组成
图1:静态结构说明图。input包含rows、format,decision与input同级;这是字段归属关系,不是执行流程或运行截图。

模块Logger怎样保留公共根字段

package main

import (
	"log/slog"
	"os"
)

// runImport 接收模块专用 Logger,不修改全局默认 Logger。
func runImport(logger *slog.Logger) {
	logger.Info("batch accepted",
		slog.Int("rows", 24),
		slog.Group("source",
			slog.String("kind", "csv"),
			slog.String("name", "sample.csv"),
		),
	)
}

func main() {
	// 使用标准输出,进程不负责关闭 os.Stdout。
	root := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	root = root.With(slog.String("service", "catalog-demo"))

	// 公共任务字段先绑定,保留在根部;后加的字段进入 import 组。
	task := root.With(slog.String("task_id", "demo-42"))
	importLogger := task.WithGroup("import").
		With(slog.String("mode", "preview"))

	runImport(importLogger)

	// task 未被 WithGroup 改写,因此这里的 status 仍在根部。
	task.Info("task finished", slog.String("status", "previewed"))
}

按代码结构,第一条记录的公共路径是 service、task_id,模块路径是 import.mode、import.rows、import.source.kind 与 import.source.name。这里的点号是本文描述对象路径的记法,不是在 JSON 中手写带点的键。时间、级别、消息由内置 Handler 处理。

WithGroup 返回派生 Logger;原来的 task 仍可用于任务级事件,因此结尾的 status 不进入 import。官方实现中的派生关系可见 https://go.dev/src/log/slog/logger.go。

根公共字段与import模块字段的静态分组
图2:静态结构说明图。公共字段与import对象分属根部和模块边界,source是模块内局部分组;连线表示包含关系,不表示日志发生顺序。

我不把所有层级都塞进组名

我最想提醒的是绑定位置。With 在进入组之前还是之后调用,会影响新属性的归属;只看最终变量名,很难看出差别。

// 片段中的 root 来自完整示例;两种绑定位置并不一样。
a := root.With("task_id", "demo-42").WithGroup("import")
b := root.WithGroup("import").With("task_id", "demo-42")
a.Info("position A", "rows", 24)
b.Info("position B", "rows", 24)

// 从 task 派生两个并列模块,不从 importLogger 派生兄弟模块。
parseLogger := task.WithGroup("parse")
storeLogger := task.WithGroup("store")
parseLogger.Info("input checked", "valid", true)
storeLogger.Info("write skipped", "reason", "preview")

在示例A中,task_id 是进入模块组之前绑定的公共字段;示例B中它在 import 内。后面的 parseLogger 与 storeLogger 从共同的 task 派生,属于并列模块。如果从 importLogger 继续 WithGroup("store"),得到的是更深的嵌套,而不是切回根部再建立兄弟组。

这也是我不按每个 Go 包名机械叠组的原因:检索者需要记住的是业务位置,不是源码目录。模块层、局部对象层通常已经能够表达本例;更多层级是否值得引入,应由字段含义决定。

让查询、阅读和开销一起可控

我会在接入日志平台之前,先写一张小型字段约定。它不依赖颜色或截图解释,复制成文本也能知道每个字段的含义;同一种计数固定名称、固定类型,避免今天是数字、明天是带单位字符串。

本例字段归属设计目的
service、task_id根部跨模块关联
import.mode、import.rows模块解释导入任务
import.source.kind、name局部来源对象描述输入来源
status任务结束记录的根部任务级结果

我会在组件构建时准备长期复用的模块 Logger,在任务开始时绑定该任务的标识,不把每条日志都重新构造一遍。官方博客介绍了 With 和 Handler 为公共属性提供优化空间;但能省多少开销取决于 Handler 与负载,这里没有跑基准,不能据此给出加速比例。

另一个容易忽略的地方是参数求值:组并不会使昂贵的字段计算自动变懒。需要时可考虑级别判断或合适的延迟表示,但不要为了让结构漂亮而在每条低级别日志里构造大对象。官方性能讨论可参考 https://pkg.go.dev/log/slog#hdr-Performance_considerations。

哪些边界值得单独留意

空组名是不是表示退出当前组?

不是。WithGroup("") 不建立新组,也不是“返回父级”。需要根级或兄弟模块日志时,保留相应的父 Logger 变量。

分组以后就不会泄露敏感值吗?

不会。分组只是结构设计,不是脱敏或访问控制。我的示例只使用合成标识和示例文件名;业务中仍应单独设计允许记录的字段,避免输出凭据、完整个人数据或不必要的输入内容。

能不能只把键写成import.rows?

这不等同于创建组,尤其当输出是JSON时。为了表达结构,我会用实际分组属性或模块子Logger,而不是依赖某个平台替我解释键中的点号。

什么时候不值得分组?

如果一个小工具只输出少量固定字段,消费者又偏好平面记录,强行叠组未必有收益。我的取舍是:先稳定公共字段和查询习惯,再引入有明确边界的模块组;不是日志越深就越专业。

这组写法适合字段可能重名、模块需要独立扩展的服务。单条对象用Group,长期模块用WithGroup,公共字段先绑定,并保留父Logger用于任务级事件——这是我在这个示例中愿意保留的最小结构。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>