当前位置:首页 > 文章列表 > Golang > Go教程 > Go log/slog.Logger.With 复用公共字段的实现方式

Go log/slog.Logger.With 复用公共字段的实现方式

来源:17golang原创 2026-11-01 20:31:45 0浏览 收藏

请求日志经常有一组重复字段:服务名、组件名、请求 ID 或租户 ID。如果每次调用 slog.Info 都重新拼一遍,字段很容易漏写;如果把请求字段塞进全局变量,又可能在并发请求之间串值。log/slog.Logger.With 的合适用法,是从一个基础 logger 派生出带公共属性的新 logger,再把一次事件才有的字段留在单条日志调用里。

官方文档:https://pkg.go.dev/log/slog#Logger.With

Logger.With 会返回一个包含附加属性的 logger;这些属性会出现在它后续的每次输出中,而原 logger 仍然可以单独使用。把稳定字段放在 With,把请求字段绑定到请求生命周期,把结果、耗时和错误留给单条事件,是最容易维护的分层方式。

先建立基础 logger 与公共字段

With 接收的参数格式和 Logger.Info 等输出方法相同,可以传交替的键和值,也可以传 slog.Attr。它返回使用同一个 Handler、但额外携带这些属性的 logger。比如组件名对一个组件内的所有日志都稳定,就可以只在初始化时绑定一次。

下面的例子使用标准库的 JSONHandler,让公共属性在结构化日志里保持为独立字段:

package main

import (
	"log/slog"
	"os"
)

func main() {
	// Handler 决定结构化日志的输出格式;这里选择 JSON 便于下游检索。
	base := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	// With 返回派生 logger,component 会进入它后续的每一条日志。
	workerLog := base.With("component", "worker")
	workerLog.Info("worker started")

	// 原 logger 没有被改写,仍可输出不带 component 的程序级日志。
	base.Info("application ready")
}

这个分层有两个实际好处。第一,公共字段只需要维护一个入口;第二,base 和 workerLog 的语义不同,调用者从变量名就能看出一条日志属于哪个范围。官方文档也把“很多日志行拥有共同属性”作为使用 Logger.With 的典型场景。

Go slog Logger.With 按原始 logger、组件 logger、请求 logger 分层追加公共属性的静态结构说明图
图1:Logger.With 的公共属性分层说明图;这是静态结构图,不是截图或运行证据。

按请求或任务派生局部 logger

请求 ID 只对一次请求有效,适合从组件 logger 再派生一层。派生动作应发生在请求入口或任务入口,而不是每一条日志都重新拼参数:

package main

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

func handle(ctx context.Context, base *slog.Logger, requestID string) {
	// requestID 的生命周期只覆盖当前处理函数,因此在入口创建局部 logger。
	requestLog := base.With("request_id", requestID)
	requestLog.InfoContext(ctx, "request accepted")

	// 只有这一条事件需要的字段,直接放在调用参数中,不扩大 logger 的生命周期。
	requestLog.InfoContext(ctx, "request finished", "result", "ok")
}

func main() {
	// 程序级字段在所有请求之间共享,但请求级字段不写回这个变量。
	base := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With("component", "api")
	handle(context.Background(), base, "req-17")
}

可以把字段分成三层:服务或组件字段是稳定公共属性;请求或后台任务字段是一次生命周期内的公共属性;某个事件的状态、结果和错误是单条调用字段。这样做比把所有字段都放进 With 更清楚,因为“会不会出现在后续每一条日志里”是一个明确的设计决定。

区分派生 logger 与原 logger

最容易误解的地方是:With 不是给原 logger 追加可变字段,而是返回另一个 logger。下面三个调用的字段集合不同:

func logLayers(ctx context.Context, base *slog.Logger) {
	// 派生 logger 只在当前变量中增加 module,不会改变 base 的字段集合。
	moduleLog := base.With("module", "billing")

	// base 只有自己的公共字段。
	base.InfoContext(ctx, "program event")

	// moduleLog 同时包含 base 的属性和 module 属性。
	moduleLog.InfoContext(ctx, "module event")

	// 从 moduleLog 继续派生,请求日志还会继承 module,但不会反向污染 moduleLog。
	requestLog := moduleLog.With("request_id", "req-42")
	requestLog.InfoContext(ctx, "request event")
}

如果希望把不同子系统的字段分开,还可以结合 WithGroup 给属性加上分组前缀。不过这篇文章只讨论公共字段复用;是否分组应根据 Handler 的输出结构和下游查询方式决定,不要为了“看起来整齐”给所有字段套一层。

处理动态字段与属性生命周期

判断一个字段放在哪里,可以先问一句:“它在当前 logger 的每一条日志里都成立吗?”如果答案是肯定的,再放入 With;如果只有某个事件成立,就保留在 InfoContext、ErrorContext 或 LogAttrs 的参数里。

字段类型建议位置原因
服务名、组件名base.With生命周期覆盖组件或进程
request_id、job_id请求入口派生的 logger只覆盖一次请求或任务
耗时、结果、重试次数单条事件调用不是该范围内每条日志都成立
密码、访问令牌不要直接记录公共属性会扩大泄漏范围

还要注意值本身的计算成本。结构化日志的调用参数可以携带适合延迟处理的值,但不要为了构造一条可能被 Handler 丢弃的低级别日志,提前做昂贵的字符串拼接。公共字段尤其应该是已经明确、可安全记录的值。

落到并发使用边界与速查表

并发处理时可以共享一个只携带稳定字段的基础 logger,然后为每个请求派生自己的 logger。真正需要避免的是共享可变的请求字段容器,或者在一个请求结束后继续复用带有旧 request_id 的局部 logger。派生 logger 的变量应跟随请求、任务或组件生命周期结束。

func serve(ctx context.Context, base *slog.Logger, jobs 

上例的重点不是让每条日志都启动 goroutine,而是展示字段边界:共享的是基础 logger,隔离的是请求属性。自定义 Handler 时还要遵守 Handler 的并发约定;官方文档明确指出 Handler 方法可能与自身或其他方法并发调用,具体同步责任由 Handler 实现承担。

Go slog 并发请求从共享基础 logger 派生独立请求 logger 并隔离 request_id 的静态说明图
图2:并发请求各自派生 logger 的边界说明图;这是静态结构图,不是截图或运行证据。

最后可以用下面这张速查表做代码评审:

  • 公共字段是否真的对该 logger 的每条日志都成立?
  • With 返回的派生 logger 是否命名清楚,原 logger 是否仍保留给其他范围?
  • 请求字段是否在请求入口创建、请求结束后不再泄漏到下一次任务?
  • 事件字段是否留在单条调用,而不是让 logger 携带过多短生命周期状态?
  • 是否把密码、令牌或其他敏感值放进了会被重复输出的公共属性?

相关问题

Logger.With 会修改原来的 logger 吗?

不会。它返回一个包含附加属性的 logger;原 logger 仍可继续使用。保留原 logger 并按范围命名,是同时记录程序级、组件级和请求级日志的基础。

request_id 应该每次 Info 都传一遍吗?

如果同一请求会产生多条日志,更适合在请求入口用 With("request_id", value) 派生局部 logger。只有某个字段只对单条事件成立时,才放在具体日志调用中。

With 和单条日志参数可以混用吗?

可以。Logger 的公共属性会和单条调用传入的属性一起形成记录。建议用生命周期来划分:稳定字段进入 With,瞬时字段留在事件调用。

并发请求能共享同一个基础 logger 吗?

可以共享只含稳定属性的基础 logger,再为每个请求派生自己的 logger。不要通过共享可变变量保存 request_id,也不要跨请求复用携带旧请求字段的局部 logger。

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