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 改成“当前文件对象”。

模块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。

我不把所有层级都塞进组名
我最想提醒的是绑定位置。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用于任务级事件——这是我在这个示例中愿意保留的最小结构。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
269 收藏
-
313 收藏
-
275 收藏
-
294 收藏
-
455 收藏
-
388 收藏
-
233 收藏
-
173 收藏
-
385 收藏
-
269 收藏
-
135 收藏
-
265 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习