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

Go slog 怎样把相关字段归到同一个 group

来源:17golang原创

时间:2026-11-15 20:29:31 482浏览 收藏

Go slog 把相关字段归入同一个 group,有两种常用写法:slog.Group("request", ...) 构造一组属性,适合只包住当前这几个字段;logger.WithGroup("request") 返回带组作用域的派生日志器,适合连续多条日志共用同一组。后一种写法会影响之后新增的属性,不能把它当作只对下一对键值生效的标记。

以商品目录服务为例,服务标识希望留在日志根层,请求编号和路由属于 request,数据库名称属于 db。先决定这些归属,再选API,能避免把本应平级的字段意外嵌到另一个组里。以下为原创示例与结构推导,没有把示例描述为本机运行结果。

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

先给请求日志划分字段归属

假设服务里到处都写 id、name 和 status,仅靠这些短字段名,查询时很难分清请求编号与其他业务编号。group 的作用是给相关属性一个稳定的归属:根层放 service,request 组放 id 与 route,db 组放 name。status 放根层还是请求组,都可以由应用约定,但同一种事件应保持一致。

标准库的 JSONHandler 把组表达为嵌套对象,TextHandler 用带点号的名称表现分组。自定义 Handler 的具体表示由它实现;不要仅凭某个采集平台展平后的显示形式,判断原始日志没有组。

本文使用 Go 1.21 已提供的 log/slog 基础接口,不依赖较新版本的 GroupAttrs。已有项目无需为了这两个分组操作额外引入日志框架。

用Group只包住一组属性

slog.Group 返回一个 Attr,它包含组名和组内属性。将它传给一次日志调用时,只有传入 Group 的那些字段属于该组;调用中紧随其后的其他属性不会自动进入它。

下例包含三条彼此对照的日志。保存为 main.go 后可以在具备相应Go工具链的项目中运行;代码只演示字段组织,不发起HTTP请求或连接数据库。

package main

import (
    "log/slog"
    "os"
)

func main() {
    // 根日志器保存跨请求不变的服务标识,JSONHandler 展示真实层级。
    base := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
        With(slog.String("service", "catalog"))

    // Group 只包住这一组字段,后面的 status 仍处于根层。
    base.Info("page ready",
        slog.Group("request",
            slog.String("id", "r-demo"),
            slog.String("route", "/items")),
        slog.Int("status", 200))

    // WithGroup 返回派生日志器;后续 With 和日志属性都进入 request。
    requestLog := base.WithGroup("request").
        With(slog.String("id", "r-demo"))
    requestLog.Info("page ready",
        slog.String("route", "/items"),
        slog.Int("status", 200))

    // 希望 request 与 db 平级时,从 base 发出日志并明确列出两组。
    base.Info("lookup complete",
        slog.Group("request", slog.String("id", "r-demo")),
        slog.Group("db", slog.String("name", "inventory")),
        slog.Int("status", 200))
}

第一条日志的业务属性结构是:根层 service 与 status,request 下是 id、route。time、level、msg 等内置字段仍由 Handler 处理。下方只画业务属性关系,不把示意图当成输出截图。

service和status处于根层而request组包含id与route的静态关系
图1:Group 属性结构说明图。request 与根层字段同属一条日志;连线表示包含关系,不表示调用先后或实际运行结果。

可以把一次调用理解为“一个组属性加若干普通属性”。这样阅读长参数列表时,就不会误以为组名会一直延续到日志调用结束。

用WithGroup复用同一作用域

第二条日志先取得 requestLog。这个日志器已经进入 request 组,之后用 With 固定的 id,以及 Info 新传入的 route、status,都处在 request 下。之前绑定到 base 的 service 仍在根层。这里的 status 与第一条位置不同,是为了直接展示两种写法的差别,实际业务应选择并坚持一种结构。

WithGroup 的返回值要接住。原来的 base 不会因为生成了 requestLog 就被改成请求日志器;要记服务级事件,仍可以继续使用 base。连续进入另一个非空组会继续嵌套,例如 requestLog.WithGroup("db") 的后续属性属于 request.db,而不是根层的 db。

派生日志器适合传给只负责某一块业务的函数,函数可以补充同组字段,无需每次重写组名。但若传进去的日志器带了哪些作用域不清楚,调用方很容易再包一层同名组。可以在函数参数约定中说明它接收根日志器还是请求日志器。

调用顺序决定已有字段在哪一层

迁移旧日志时,最容易忽略的是 With 与 WithGroup 的顺序。WithGroup 不会把之前已经固定的全部属性搬到新组;后续新增属性才受到新的组作用域约束。下面是接在完整示例 base 后面的独立对照片段,不是另一个可单独运行的 main 文件。

// 已有固定字段在进入组之前绑定,所以 service 保持在根层。
outer := base.With(slog.String("region", "east")).WithGroup("request")
// id 属于 request;region 仍在根层。
outer.Info("accepted", slog.String("id", "r-demo"))

// 相反,先进入组再 With,region 也属于 request。
inner := base.WithGroup("request").With(slog.String("region", "east"))
inner.Info("accepted", slog.String("id", "r-demo"))

outer 对应根层 service、region 与 request.id;inner 对应根层 service 与 request.region、request.id。这是 API 作用域的区别,不是属性排序风格的区别。调整方法链时,应先看字段路径是否改变,再考虑代码看起来是否简短。

base与requestLog派生日志器的固定字段和新增字段作用域
图2:日志器作用域说明图。base 保留服务标识,requestLog 关联请求字段;边界表达字段归属,没有时间轴或运行输出。

如果期望 request 和 db 平级,完整例子的第三条写法更直观:从 base 发出一次事件,分别传入两个 Group。不要先进入 request,再期待一个 db Group 自动跳回根层。

按任务选择分组并避免重复键

整理已有日志时,可以先列字段归属,再决定哪些值值得固定到派生日志器。跨很多事件不变的 service 放到 base.With;多个连续事件都属于同一作用域时再派生 WithGroup;只出现一次的局部对象则用 Group。最后查看设计中的字段路径,确认它与查询端预期一致。

group 不是同名字段的自动去重器。不要在同一条事件的同一作用域反复写 id,或者先把 request.id 固定到日志器,再在日志调用里又写同名 id 并期待它一定覆盖。采集端对重复键的处理可能不同,应该从写入端避免含糊结构。

也不要用随机请求编号作组名,如按请求生成不同的组名称。把组名固定为 request,把编号放进 id 值,字段结构才便于持续查询。用户输入适合作为经过必要处理的值,不适合随意扩展日志字段名称。

日志分组本身不承诺性能提升。With 允许 Handler 处理复用属性,但分组数量、值类型与采集链路都会影响成本;本文只给字段组织方案,不给未经测量的速度比较。

分组字段速查与常见问题

任务写法注意点
一条事件里包住几项字段slog.Group("request", ...)不改变同一调用中其他普通属性的归属
后续事件都补充同一组字段base.WithGroup("request")接住返回值,之后的With及日志属性受组约束
服务字段留根层先With(service)再WithGroup先固定的字段不自动搬入后来的组
两个组平级在base同一次调用中传两个Group不要在已进入的作用域中误加一层
退出当前组改用保存的base或较外层日志器WithGroup("")不会退出,它返回接收者

组名为空会怎样? WithGroup("") 不会建立新层,也不会弹出已有层。Group 的空键表示将组内属性内联;这是另一种语义,不应拿空字符串模拟退组。

空组会输出一个空对象吗?按 Handler 的约定,没有属性的组会被忽略。不要依靠一个空Group作为“该阶段已执行”的业务信号;需要状态时应明确记录一个有意义的属性或事件。

为什么TextHandler里看到request.id?这是文本Handler表示组字段的方式。JSONHandler会使用对象层级。若应用把键本身写成带点字符串,不能假定它与真正的嵌套组在所有输出和查询端都等价。

所有字段都应该分组吗?没有必要。优先给有稳定语义的对象分组,避免多层嵌套只是为了缩短字段名。让根字段、request和db的职责能被读者直接解释,通常比层数更多更有用。

Go项目结构化日志介绍:https://go.dev/blog/slog

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