当前位置:首页 > 文章列表 > Golang > Go问答 > Go slog.With 为什么会复制属性

Go slog.With 为什么会复制属性

来源:17golang原创 2026-11-14 16:55:50 0浏览 收藏

如果你在请求入口写了 requestLogger := logger.With("request_id", id),然后发现原来的 logger 没有这个字段,这不是属性丢失,而是 slog.With 的设计:它派生一个新 Logger,把属性交给 Handler 保存或预格式化,原 Logger 继续保持原来的上下文。

这里说的“复制属性”也不是简单地把一份全局 map 复制出来。Logger.With 复制的是 Logger 外壳,并调用 Handler.WithAttrs 生成新的 Handler 状态;自定义 Handler 如果错误地复用可变切片,就会把派生 Logger 和原 Logger 绑在一起。

一句话结论:With 负责派生 Logger,WithAttrs 负责派生 Handler;正确实现必须让原对象不变,同时把旧属性和新属性按顺序组合起来。

Logger.With 复制的是外壳,不是修改原对象

官方实现中的 Logger 只有一个 Handler 字段。调用 With 时,先浅复制 Logger 结构,再把 l.handler.WithAttrs(argsToAttrSlice(args)) 的返回值放入副本。参数会按普通日志参数规则转换为 slog.Attr。

package main

import (
	"log/slog"
	"os"
)

func main() {
	// 关闭时间字段,便于读者只观察 Logger 派生出来的属性。
	h := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey {
				return slog.Attr{} // 静态示例不关心时间字段。
			}
			return a
		},
	})
	base := slog.New(h)
	request := base.With("request_id", "r-17")

	base.Info("from base")
	request.Info("from request", "route", "/orders")
}

上面两个 Logger 共享底层输出目标,但它们的 Handler 状态不同。base 的日志没有 request_id,request 的每条日志都会带上它。把派生 Logger 赋回变量只是改变当前变量指向,不会反向修改已经存在的其他 Logger。

WithAttrs 为什么必须返回新的 Handler

Logger.With 不直接保存一份属性列表,而是把属性交给 Handler。Handler 接口明确要求 WithAttrs 返回一个包含旧属性和新属性的新 Handler,接收者本身不能被修改。这样同一个基础 Logger 才能安全地派生出多个互不干扰的上下文。

slog Logger With 与 Handler WithAttrs 派生关系的静态结构说明图
图1:结构说明图,展示 Logger、Handler、With、WithAttrs 与派生属性状态的静态关系;不是运行截图。

属性顺序也有意义。一次 base.With("service", "pay") 再调用 With("request_id", "r-17"),派生 Handler 需要保留前后顺序;随后日志调用传入的 Record 属性再接在 Logger 已有属性之后。Handler 可以按自己的格式输出,但不能把原 Logger 的状态改成“最后一次派生”的结果。

这就是下面两种写法的差别:

  • requestLogger := base.With(...):推荐,基础上下文可继续复用。
  • base = base.With(...):也合法,但当前变量从此代表派生 Logger;其他已经保存的 Logger 不受影响。

内置 Handler 为什么看起来像“复制了属性”

TextHandler 和 JSONHandler 会利用 WithAttrs 做一次性工作。官方源码中的公共 Handler 状态会被克隆,属性可以被预格式化,后续每条日志只需处理新 Record 的动态字段。这是性能优化,也是为什么属性并不一定以原始 []slog.Attr 的形态留到 Handle。

slog WithAttrs 预格式化与 Handle 动态记录属性关系说明图
图2:关系说明图,对比 WithAttrs 的固定属性、预格式化缓存与 Handle 接收的动态 Record;不是运行结果截图。

因此,With 适合放请求 ID、租户 ID、服务名这类会反复出现的固定属性。昂贵的动态值仍会在调用参数求值阶段产生;如果日志级别会过滤掉这条记录,参数表达式也不会因为 With 而自动延迟。

还要区分属性值本身的生命周期。slog.Attr 是值类型,但 slog.Any 里面仍可能装着指向可变对象的指针或切片。With 解决的是 Logger/Handler 状态派生,不会替你深拷贝业务对象;需要跨请求或异步使用时,仍应明确对象的所有权。

自定义 Handler 要避免切片别名

自定义 Handler 最容易出错的地方,是用 append 直接在接收者的切片上追加属性。只要底层数组还有容量,后续派生 Logger 就可能覆盖同一块内存,造成属性串线。实现时要复制 Handler 结构,并为保存属性的切片分配独立容量。

type attrsHandler struct {
	base slog.Handler
	attrs []slog.Attr
}

func (h *attrsHandler) WithAttrs(more []slog.Attr) slog.Handler {
	// 复制接收者的切片,避免派生 Handler 覆盖原 Handler 的底层数组。
	all := make([]slog.Attr, 0, len(h.attrs)+len(more))
	all = append(all, h.attrs...)
	all = append(all, more...)
	return &attrsHandler{base: h.base, attrs: all}
}

func (h *attrsHandler) WithGroup(name string) slog.Handler {
	// 组名由底层 Handler 负责;当前包装器只复制自己的属性状态。
	return &attrsHandler{base: h.base.WithGroup(name), attrs: append([]slog.Attr(nil), h.attrs...)}
}

func (h *attrsHandler) Enabled(ctx context.Context, level slog.Level) bool {
	// 过滤规则继续交给底层 Handler,避免包装器改变级别语义。
	return h.base.Enabled(ctx, level)
}

func (h *attrsHandler) Handle(ctx context.Context, r slog.Record) error {
	// 这里省略具体格式化;生产实现应把 h.attrs 与 r.Attrs 合并后交给输出层。
	return h.base.Handle(ctx, r)
}

这个片段只展示状态复制边界。真正的包装 Handler 还需要决定固定属性如何参与输出、是否调用 Record.Clone、如何处理 WithGroup,以及如何保持并发写入安全。最重要的规则只有一个:WithAttrs 不修改接收者,返回的新对象可以拥有自己的属性存储。

请求级日志应该怎样派生

在 HTTP 或消息消费入口,通常保留一个不带请求字段的基础 Logger,再为每个请求创建派生 Logger。若不同子系统可能使用相同键名,可以继续用 WithGroup 建立命名空间;它同样返回新的 Logger,并把分组交给 Handler 处理。

func handle(logger *slog.Logger, requestID string) {
	// 每次调用创建独立上下文,不把请求字段写回共享的基础 Logger。
	requestLogger := logger.With("request_id", requestID)
	parserLogger := requestLogger.WithGroup("parser")

	parserLogger.Info("decode payload", "size", 128)
}

可以用下面这张清单检查实现:

场景推荐做法注意点
共享服务字段基础 Logger.With固定属性只从基础上下文派生
请求字段入口处创建派生 Logger不要把请求状态写进全局 Logger
自定义 HandlerWithAttrs 返回新实例复制切片,避免底层数组别名
子系统同名键WithGroup 建立命名空间确认 TextHandler 与 JSONHandler 的展示差异

相关问题

With 会深拷贝 slog.Any 里的对象吗?不会。它派生 Logger/Handler 状态;属性值内部引用的业务对象仍由调用方负责生命周期。

为什么自定义 Handler 里要复制 attrs 切片?因为派生 Handler 不能修改原 Handler,直接 append 可能复用底层数组,导致不同 Logger 之间互相覆盖属性。

With 和每次 Info 传属性有什么区别?With 适合重复出现的固定字段,Handler 有机会预格式化;Info 传入的属性属于当前 Record,更适合一次性的动态字段。

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