当前位置:首页 > 文章列表 > Golang > Go教程 > Go log/slog 结构化日志字段的分层设计

Go log/slog 结构化日志字段的分层设计

来源:17golang原创 2026-10-15 09:26:55 0浏览 收藏

我在排查一条请求日志时遇到过一个很典型的现象:同一个服务里,有的记录带 request_id,有的把它拼进消息文本;业务字段有时叫 user_id,有时又藏在一段 JSON 里,最麻烦的是调试登录问题时差点把令牌写进日志。用 Go log/slog 时,问题通常不在“会不会打印”,而在字段应该在哪一层产生。

可维护的做法是把字段分成服务层、请求层和事件层:服务层用 Logger.With 固定,请求层用 WithGroup 建立命名空间并显式提取 context 属性,事件层只记录当前动作;最后在 Handler 统一脱敏。

官方资料:https://pkg.go.dev/log/slog

要点速览
  • Handler 是日志输出和字段策略的后端,Logger 负责收集记录。
  • context.Context 不会被 InfoContext 自动展开,想写入哪些字段必须显式提取。
  • With 适合固定字段,WithGroup 适合给请求或子系统建立命名空间。
  • ReplaceAttr 可以在 Handler 层集中处理 password、token 这类敏感字段。

先把字段按生命周期分成三层

先不要急着设计 key 名。把字段按“会变化多快”分层,后面的 API 选择会清楚很多:

层次典型字段产生位置适合的 slog 写法
服务层service、version、region进程启动后基本不变logger.With
请求层request_id、trace_id、route一次请求或一次任务WithGroup 加显式提取
事件层user_id、item_count、err某个具体动作LogAttrs 或单次日志参数

这样分层的好处是:服务字段不会在每行日志里重复拼接,请求字段不会污染全局 logger,业务字段也不会被错误地留在长生命周期对象上。

Go log/slog 将 service、request、event 字段分层后通过 With、WithGroup 汇入 Handler 的静态结构说明图
图1:log/slog 字段生命周期与 Handler 分层说明图,不是运行截图。

用 With 与 WithGroup 建立稳定命名空间

Logger.With 返回一个带附加属性的新 Logger,原来的 Logger 不会被改写;WithGroup 则为后续字段增加命名空间。下面这段代码把三层字段放在各自的位置:

package main

import (
	"context"
	"log/slog"
	"os"
)

func logOrder(ctx context.Context, base *slog.Logger, requestID string) {
	// 服务层字段在进程范围内稳定,创建一次即可复用。
	serviceLog := base.With(
		slog.String("service", "checkout"),
		slog.String("region", "cn-east-1"),
	)

	// 请求层字段放进 request 命名空间,避免与业务字段同名。
	requestLog := serviceLog.WithGroup("request").With(
		slog.String("request_id", requestID),
	)

	// 事件层只记录这一次动作;InfoContext 不会自动展开 ctx。
	requestLog.InfoContext(ctx, "order accepted",
		slog.String("order_id", "o-1038"),
		slog.Int("item_count", 3),
	)
}

func main() {
	// JSONHandler 负责把结构化字段写成每行一个 JSON 对象。
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	logOrder(context.Background(), logger, "req-7f2a")
}

使用 JSON Handler 时,输出会保留分组层次,大致呈现为 service、region、request.request_id 和事件字段。这里的重点不是固定某个输出排版,而是让字段的来源可预测。若一个子系统也有 id,可以继续使用 WithGroup("parser"),避免同名 key 互相覆盖或让查询含义变得模糊。

明确 context 只负责传递现场信息

很多日志“偶尔缺 request_id”,根因是把 context 当成了自动字段容器。InfoContext、LogAttrs 的确会把 context 传给 Handler,但 Handler 是否读取、读取哪些值,取决于你的实现。标准 JSON Handler 不会把 context 中所有值扫出来。

因此,推荐在请求入口或日志适配层做一次明确转换:

type requestContextKey struct{}

func withRequestID(ctx context.Context, id string) context.Context {
	// 使用私有 key,避免和其他包的 context 值发生碰撞。
	return context.WithValue(ctx, requestContextKey{}, id)
}

func requestID(ctx context.Context) (string, bool) {
	// 只提取日志约定的字段,不把 context 当作 map 整体输出。
	id, ok := ctx.Value(requestContextKey{}).(string)
	return id, ok && id != ""
}

func infoWithContext(logger *slog.Logger, ctx context.Context, msg string) {
	attrs := make([]slog.Attr, 0, 1)
	if id, ok := requestID(ctx); ok {
		// 显式加入 request_id,字段来源在代码里一眼可见。
		attrs = append(attrs, slog.String("request_id", id))
	}
	logger.LogAttrs(ctx, slog.LevelInfo, msg, attrs...)
}

如果你的链路追踪库已经在 context 中保存了 trace ID,也应该通过一个小的适配函数显式取出并命名。这样做有两个好处:没有值时行为明确,字段名变更时只需要修改适配层;同时不会因为某个库把额外对象放进 context,就意外把内部数据写入日志。

在 Handler 层统一保护敏感字段

脱敏最好放在输出边界,而不是要求每个调用点都记住“密码必须改名”。HandlerOptions.ReplaceAttr 可以在属性即将输出前按 key 做统一替换;分组路径会通过 groups 参数传入,所以可以根据命名空间细化规则。

func redactingHandler(out io.Writer) slog.Handler {
	// 这些 key 即使出现在不同业务模块,也统一输出固定占位符。
	redact := func(groups []string, attr slog.Attr) slog.Attr {
		switch strings.ToLower(attr.Key) {
		case "password", "token", "authorization", "secret":
			// 不保留原值,避免日志采集、转储或错误回显再次暴露秘密。
			return slog.String(attr.Key, "[REDACTED]")
		default:
			return attr
		}
	}

	return slog.NewJSONHandler(out, &slog.HandlerOptions{
		// 统一在 Handler 边界执行,调用方仍可使用正常字段名。
		ReplaceAttr: redact,
	})
}

上面的示例需要导入 io 和 strings。真实项目里还要决定是否按 groups 限定规则,例如只对 auth 分组下的 token 脱敏。不要只依赖字段名:一个叫 credential 的结构体可能通过 LogValuer 展开出多个敏感属性,应该在类型自己的 LogValue 方法里返回已经整理过的安全值。

Go log/slog 显式提取 context 中的 request_id 与 trace_id,并在 ReplaceAttr 中将 password 和 token 变成 redacted 的静态关系说明图
图2:上下文字段提取与敏感字段脱敏关系说明图,不是运行截图。

用一条检查清单复查输出

字段分层完成后,我会用下面几项快速复查,而不是只看某一行日志“长得像不像 JSON”:

  • 来源:每个 request 或 trace 字段是否都有明确的提取入口,缺失时是否允许为空?
  • 命名:同一概念是否只保留一个 key,子系统是否用 WithGroup 隔离?
  • 生命周期:服务级 logger 是否不会携带请求级 ID,事件级大对象是否没有被放进 With?
  • 安全性:password、token、authorization、secret 以及自定义 LogValuer 是否都经过脱敏?
  • 并发性:自定义 Handler 是否返回新实例而不是修改共享 receiver,并且能安全处理并发调用?

最后一个问题尤其容易被忽略。官方 Handler 约定允许多个方法并发调用;如果自己包装 Handler,应保持原 Handler 的 WithAttrs、WithGroup 语义,返回带新状态的 Handler,再把记录交给下层处理。这样,字段结构才不会因为请求并发而互相串线。

常见问题

为什么调用 InfoContext 后日志里没有 context 的全部字段?

因为 context 只是传给 Handler 的现场参数,不是自动展开的属性集合。需要在适配层显式提取 request_id、trace_id 等允许写入的字段。

固定字段都应该放到 With 里吗?

只放真正稳定、体积小且不会随请求变化的字段。请求 ID、用户 ID 和大对象应放在请求或事件层,避免长生命周期 Logger 持有错误上下文。

ReplaceAttr 能替代所有脱敏逻辑吗?

不能。它适合按属性 key 做统一规则;复杂结构或自定义类型还需要在 LogValuer 中主动返回安全表示,并在日志边界做一次整体复查。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go os/signal 处理服务退出信号的生命周期Go os/signal 处理服务退出信号的生命周期
上一篇
Go os/signal 处理服务退出信号的生命周期
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    479次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    535次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    557次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    507次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    328次使用