登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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

来源:17golang原创

时间:2026-11-01 20:31:45 376浏览 收藏

请求日志经常有一组重复字段:服务名、组件名、请求 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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>