当前位置:首页 > 文章列表 > Golang > Go教程 > Go log/slog 怎样给每个请求添加上下文

Go log/slog 怎样给每个请求添加上下文

来源:17golang原创 2026-10-14 01:44:06 0浏览 收藏

在 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 传给日志调用。这样下游函数既能响应取消,也能稳定拿到同一组字段。

HTTP请求、context.Context、请求级slog Logger与Handler之间的静态关系图
图1:请求属性进入 Logger、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 是比交替传入键和值更明确的选择;但不要为了追求形式而把可读性很差的属性列表塞进一行。先保持字段语义清楚,再针对真实的日志开销做优化。

Context、Logger.With、slog.Group、LogAttrs与JSONHandler边界关系说明图
图2:Context、请求 Logger、结构化属性和 Handler 的边界说明图,不是运行截图。

请求结束和取消时要注意什么

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 服务可以直接采用的起点。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go slog.Handler 怎样过滤敏感字段Go slog.Handler 怎样过滤敏感字段
上一篇
Go slog.Handler 怎样过滤敏感字段
Go net/http 怎样设置请求体大小上限
下一篇
Go net/http 怎样设置请求体大小上限
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    463次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    527次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    545次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    494次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    313次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码