Go log/slog 怎样为 slog 添加请求上下文字段
来源:17golang原创
时间:2026-10-18 07:31:21 252浏览 收藏
为 slog 添加请求字段,可以在请求入口从 context.Context 取出约定的请求标识,再用 base.With(slog.String("request_id", id)) 派生请求专属 Logger,向下传递并调用 InfoContext。传入 Context 与输出它里面的值是两件事:内置 JSONHandler 不会自动把任意 Context 值变成日志字段。
这个做法适合请求标识在入口确定、多个业务函数需要复用它的服务。下面使用标准库实现明确的字段绑定,不包装自定义 Handler,也不依赖第三方日志库。官方文档:https://pkg.go.dev/log/slog。
先确定谁负责把请求字段变成日志属性
把职责分成三层:Context 携带请求数据;入口函数选择允许记录的字段;Logger 复用已经绑定的属性。事件本身的属性仍在具体日志调用处补充。例如 service 属于基础 Logger,request_id 属于请求 Logger,operation 属于某条事件。
| 动作 | 负责什么 | 不负责什么 |
|---|---|---|
| Context.Value | 按约定键读取请求值 | 不是日志属性枚举器 |
| Logger.With | 返回绑定属性的 Logger | 不会修改原 Logger 的请求字段 |
| InfoContext | 把 Context 交给日志处理链 | 不承诺自动提取所有业务值 |

在请求入口派生 Logger 并向下传递
保存下面的完整程序为 main.go。requestIDKey 是本包私有键类型;字段提取函数只认识这一种键。实际服务应由可信入口取得或生成请求标识,再通过同一组封装函数设置和读取;不要让各业务包各自定义一个看似同名、实际不同的键类型。
package main
import (
"context"
"log/slog"
"os"
)
// 私有键类型避免与其他包常用的字符串键发生碰撞。
type requestIDKey struct{}
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func requestLogger(ctx context.Context, base *slog.Logger) *slog.Logger {
// 只提取约定字段,不尝试枚举整个 Context。
id, ok := ctx.Value(requestIDKey{}).(string)
if !ok || id == "" {
return base
}
return base.With(slog.String("request_id", id))
}
func handle(ctx context.Context, base *slog.Logger) {
// 每个请求绑定自己的值;base 始终不变。
logger := requestLogger(ctx, base)
logger.InfoContext(ctx, "请求进入")
lookup(ctx, logger)
}
func lookup(ctx context.Context, logger *slog.Logger) {
// 事件字段只属于本条日志,请求字段由 logger 复用。
logger.InfoContext(ctx, "查询准备", slog.String("operation", "lookup"))
}
func main() {
// 演示写到标准输出;日志器不拥有也不关闭这个输出流。
base := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
With(slog.String("service", "catalog-demo"))
// 两个固定标识仅供示例,不是生产请求编号生成方案。
ctxA := withRequestID(context.Background(), "demo-A")
ctxB := withRequestID(context.Background(), "demo-B")
handle(ctxA, base)
handle(ctxB, base)
}
handle 同时接收 Context 与 Logger:前者保留请求取消和期限等语义,后者声明日志依赖。lookup 没有重新读取请求字段,因而不会在每个事件上重复实现键名、缺失值和字段白名单规则。若现有代码本来就显式传递请求 ID,也可以直接用该参数派生 Logger,不必为了日志把所有参数塞进 Context。
用两组请求观察字段是否串用
# 在 main.go 所在目录执行;本文未实际运行此示例。 go run main.go
按代码结构,正常输出时应有四条日志事件:demo-A 的两条带同一 request_id,demo-B 的两条带另一个 request_id,每条均带 service;只有“查询准备”事件带 operation。时间值由运行环境产生,不应与固定样本逐字比较。这是供读者运行后的判断条件,本文不提供虚构输出。
检查时重点看属性归属:若下层日志缺少 request_id,先看是否误用了 base 或包级 slog.InfoContext;若 A 的字段出现在 B 的记录里,先看请求 Logger 是否被保存成共享变量。示例故意顺序调用两个请求,不声称已经覆盖并发压测或输出故障。

避免把请求状态写进全局 Logger
不要在每个请求里调用 slog.SetDefault 来“切换当前请求”。默认 Logger 是进程范围的选择,不是请求局部变量。也不要忽略 base.With(...) 的返回值,然后继续使用 base 并期待它已经包含请求字段。把派生结果保存在局部变量中,依赖关系会更直接。
这种设计的代价是函数签名多一个 Logger 参数;收益是字段来源可见,避免隐式依赖。适合在入口固定的请求属性只绑定一次。如果认证后才得到新的可记录信息,应在那个边界派生新的 Logger 并传给后续函数;之前的 Logger 不会自动更新,之前的日志也不会被补写。
示例只放字符串属性。不要把整个请求、认证头、Cookie、请求体或可能被继续修改的业务对象直接交给 slog.Any。先确定字段白名单和脱敏策略;请求关联标识也不应直接当作高基数指标标签。本方案只做日志关联,不自动提供分布式追踪或日志写入可靠性保证。
请求日志的几个边界问题
没有 request_id 时怎么办?
示例返回 base,不伪造一个“unknown”冒充真实关联值。业务可以改为显式缺失标记,但要统一约定;错误类型或空字符串都会按缺失处理。
Context 取消后,日志一定不再输出吗?
不要把日志调用当作业务取消检查。需要停止业务时应单独检查取消信号;传 Context 的目的,是给处理链保留上下文,不保证它等同于日志开关。
后续再传一个 request_id 会覆盖原值吗?
不要依赖重复键实现覆盖。入口绑定后,事件层应禁止再写同名请求键,否则可能产生重复 JSON 键和下游解析歧义。需要新作用域时,明确字段名或派生策略。
落地时可以沿三条线检查自己的代码:入口是否只有一个字段提取约定;业务函数是否接收并使用正确 Logger;事件层是否只补充事件属性。Context 键与值的用途可参阅 https://pkg.go.dev/context#WithValue,slog 的设计背景见 https://go.dev/blog/slog。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习