Go log/slog 结构化日志字段的分层设计
我在排查一条请求日志时遇到过一个很典型的现象:同一个服务里,有的记录带 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,业务字段也不会被错误地留在长生命周期对象上。

用 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 方法里返回已经整理过的安全值。

用一条检查清单复查输出
字段分层完成后,我会用下面几项快速复查,而不是只看某一行日志“长得像不像 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 中主动返回安全表示,并在日志边界做一次整体复查。
Go os/signal 处理服务退出信号的生命周期
- 上一篇
- Go os/signal 处理服务退出信号的生命周期
- 下一篇
- 暂无
-
- Golang · Go教程 | 1小时前 |
- Go os/signal 处理服务退出信号的生命周期
- 322浏览 收藏
-
- Golang · Go教程 | 1小时前 | Go crypto/tls 证书到期时间 x509.Certificate NotAfter
- Go crypto/tls 读取证书到期时间的代码写法
- 183浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go net/netip 解析地址并区分 IPv4 映射
- 377浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go container/heap 实现定时任务优先队列
- 395浏览 收藏
-
- Golang · Go教程 | 1小时前 | Go 切片排序 稳定排序 sort.SliceStable
- Go sort.SliceStable 保持同键顺序的排序实现
- 300浏览 收藏
-
- Golang · Go教程 | 1小时前 | 配置 · 错误处理 · Go教程 · Go 超时配置 time.Duration time.ParseDuration 配置时长
- Go time.ParseDuration 读取配置时长的错误处理
- 119浏览 收藏
-
- Golang · Go教程 | 1小时前 | goroutine · go · Context · Go 定时任务 reset goroutine泄漏 time.Ticker Stop
- Go time.Ticker 长时间服务的停止与泄漏规避
- 121浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go time.Location 处理业务时区的转换方法
- 239浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go 构建标签拆分平台实现的组织方式
- 476浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 479次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 535次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 557次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 507次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 328次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- golang xorm 自定义日志记录器之使用zap实现日志输出、切割日志(最新)
- 2023-02-24 432浏览
-
- Go语言常用的打log方式详解
- 2023-02-24 485浏览
-
- Go常用技能日志log包创建使用示例
- 2023-01-28 105浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
