Go log/slog 怎样给每个请求添加上下文
来源:17golang原创
时间:2026-10-14 01:44:06 136浏览 收藏
在 HTTP 服务里,一条错误日志如果只有 msg=database timeout,排查时还要回头猜它属于哪个请求。使用 Go 的 log/slog 时,比较稳妥的做法是:在请求入口建立一个带有固定请求属性的 Logger,把 context.Context 和这个 Logger 一起显式传给下游;写日志时使用 InfoContext、ErrorContext 或 LogAttrs。
这里有一个容易混淆的边界:slog 的标准 Handler 不会自动扫描 Context 并把其中的值变成日志字段。Context 负责贯穿请求和取消信号,Logger.With 负责附加结构化属性,两者要在调用代码里明确组合。
官方地址:https://pkg.go.dev/log/slog
先把请求范围内不变的字段固定下来
一条请求通常有一组在整个调用链里都不变的字段,例如请求 ID、HTTP 方法、路径和租户标识。这些字段不应该在每一次 Info 调用中重复拼接,而应该在入口处通过 Logger.With 形成一个请求级 Logger。
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
"os"
)
// newRequestID 在没有上游请求 ID 时生成一个短的随机标识。
func newRequestID() string {
var raw [8]byte
if _, err := rand.Read(raw[:]); err != nil {
// 生成失败时返回固定占位值,避免把随机错误写进响应流程。
return "request-id-unavailable"
}
return hex.EncodeToString(raw[:])
}
// requestLogger 从入口请求提取稳定属性,不把整个 Request 放进日志字段。
func requestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = newRequestID()
}
return base.With(
slog.Group("request",
slog.String("id", requestID),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
),
)
}
func handler(base *slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Request.Context 负责取消和超时;请求属性由 Logger.With 显式附加。
logger := requestLogger(base, r)
ctx := r.Context()
logger.InfoContext(ctx, "request started")
handleBusiness(ctx, logger)
logger.InfoContext(ctx, "request finished")
w.WriteHeader(http.StatusNoContent)
})
}
func handleBusiness(ctx context.Context, logger *slog.Logger) {
// 下游同时接收 ctx 和 logger,避免依赖隐式的全局请求状态。
logger.InfoContext(ctx, "business handler entered", slog.String("component", "orders"))
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
http.ListenAndServe(":8080", handler(logger))
}
这段代码里有两个独立动作。requestLogger 把请求属性绑定到新的 Logger;InfoContext 则把当前请求的 Context 传给日志调用。这样下游函数既能响应取消,也能稳定拿到同一组字段。

为什么不能只把值塞进 Context
很多项目会把 request_id 放入 Context,然后直接调用 slog.InfoContext(ctx, "query"),期待输出自动带上这个值。这个期待并不成立。Context 值只有在你的 Handler 或调用代码主动读取、再转换为 slog.Attr 时,才会出现在日志中。
如果某个字段是日志链路的公共维度,直接绑定到 Logger 更清楚;如果它是取消、截止时间或链路传播需要的请求状态,则继续放在 Context。可以把职责简单分成下面几类:
| 信息 | 推荐位置 | 原因 |
|---|---|---|
| request_id、method、path | 请求级 Logger | 会重复出现在多条日志中,适合一次绑定 |
| 取消信号、截止时间 | context.Context | 由下游操作共同遵守,不是日志字段仓库 |
| 当前 SQL 行数、响应状态码 | 单次日志 Attr | 只在特定事件发生时有意义 |
| 密码、完整 Cookie、令牌 | 不记录或先脱敏 | 避免把凭据扩散到日志系统 |
如果确实需要让自定义 Handler 从 Context 读取链路信息,应该把这个行为写成 Handler 的明确契约,而不是让所有调用者猜测。使用内置 JSONHandler 时,最容易维护的方案仍然是入口处 With,调用处使用 InfoContext。
让下游日志保持同一组字段
请求 Logger 需要沿着调用链传递。不要在每个函数里重新调用 slog.Default(),也不要为了省一个参数把 Logger 放进 Context;显式参数能让依赖关系在函数签名上可见。
func loadOrder(ctx context.Context, logger *slog.Logger, orderID string) error {
// 单次事件字段只在当前日志中出现,不污染整个请求 Logger。
logger.DebugContext(ctx, "loading order", slog.String("order_id", orderID))
order, err := findOrder(ctx, orderID)
if err != nil {
// 错误对象可以记录,但要确认其中没有密码、Token 等敏感内容。
logger.ErrorContext(ctx, "load order failed",
slog.String("order_id", orderID),
slog.Any("err", err),
)
return err
}
logger.InfoContext(ctx, "order loaded",
slog.String("order_id", order.ID),
slog.Int("item_count", len(order.Items)),
)
return nil
}
func findOrder(ctx context.Context, orderID string) (Order, error) {
// 真实项目中在这里执行数据库查询,并把 ctx 传给驱动以响应取消。
return Order{ID: orderID}, nil
}
type Order struct {
ID string
Items []string
}
这种写法让同一请求的日志天然共享 request.id、request.method 和 request.path,而 order_id、item_count 只附着在具体事件上。字段层级稳定后,日志查询可以按请求 ID 聚合,也可以按组件或订单号筛选。
用 Group 和 LogAttrs 保持字段结构
slog.Group 适合给相关字段划定命名边界。使用 JSONHandler 时,下面的请求字段会位于 request 对象内;使用 TextHandler 时,通常表现为带点号的键名。这样可以避免大型项目中多个子系统都使用 id 时发生歧义。
import "time"
func recordResponse(ctx context.Context, logger *slog.Logger, status int, elapsed time.Duration) {
// LogAttrs 直接接收 Attr,适合已经确定字段类型的结构化事件。
logger.LogAttrs(ctx, slog.LevelInfo, "request response",
slog.Group("response",
slog.Int("status", status),
slog.Duration("elapsed", elapsed),
),
)
}
上面的示例需要导入 time。如果日志调用非常频繁,LogAttrs 是比交替传入键和值更明确的选择;但不要为了追求形式而把可读性很差的属性列表塞进一行。先保持字段语义清楚,再针对真实的日志开销做优化。

请求结束和取消时要注意什么
r.Context() 会在客户端断开、服务端超时或请求处理结束时发出取消信号。把它传给数据库、HTTP 客户端和下游函数,可以让这些操作及时停止;但日志是否写入成功仍由 Handler 和输出目标决定,不能把“调用了 ErrorContext”当成“日志已经被远端采集”。
还有三个常见边界:
- 不要把
*http.Request、响应体或大对象整体作为日志字段;提取少量稳定属性即可。 - 不要把用户输入直接当成字段名,也不要原样记录 Authorization、Cookie、密码和会话令牌。
- 不要在 goroutine 中继续使用已经失效的请求 Context;如果任务有意脱离请求生命周期,应先明确它的取消策略和日志归属。
如果 HandlerOptions 设置了最低日志级别,还要记住日志参数会先被求值。昂贵的字符串拼接、序列化或调试数据构造,可能在日志被丢弃时仍然发生;可以传入结构化值、使用 LogValuer,或者在真正需要时才计算。
一份可复用的落地清单
| 检查点 | 推荐做法 |
|---|---|
| 请求公共字段 | 入口处用 Logger.With 一次绑定,必要时用 slog.Group 归组 |
| Context 传递 | 下游函数显式接收 context.Context,日志使用 InfoContext 等方法 |
| 事件字段 | 使用类型明确的 slog.String、slog.Int 或 LogAttrs |
| 错误记录 | 记录可定位信息,先确认错误文本不含敏感数据 |
| 性能处理 | 避免无条件计算昂贵字段,先观察真实日志开销再优化 |
几个容易混淆的问题
一定要把 Logger 放进 Context 才能跨函数传递吗?
不需要。更直观的方式是把 Logger 作为显式参数传递。Context 仍然作为第一个参数传递给需要取消、截止时间或链路信息的操作。
调用 InfoContext 后,Context 里的 request_id 会自动出现在 JSON 日志中吗?
不会。标准 Handler 不会自动枚举 Context 值。把 request_id 转成 slog.Attr 并绑定到请求级 Logger,或者实现明确读取 Context 的自定义 Handler。
每次日志都重新调用 logger.With 会有问题吗?
功能上可以,但公共字段会重复构造,也容易让字段层级失去一致性。稳定的请求字段在入口绑定,单次事件字段在具体日志调用处追加。
应该记录完整 URL 和请求头吗?
通常不应该默认记录。完整 URL 可能包含查询参数中的个人信息,部分请求头包含认证材料。按排障需要选择字段,并在进入日志 Handler 前做脱敏。
把请求 Logger 和 Context 分开理解,代码就会清楚很多:Context 负责请求生命周期,Logger 负责结构化字段,Handler 负责最终格式化和输出。入口处建立一次请求范围 Logger,下游显式传递并使用 InfoContext,就是大多数 HTTP 服务可以直接采用的起点。
-
234 收藏
-
231 收藏
-
248 收藏
-
230 收藏
-
412 收藏
-
193 收藏
-
454 收藏
-
211 收藏
-
350 收藏
-
299 收藏
-
364 收藏
-
147 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习