Go slog 的 WithAttrs 为什么没有出现在文本输出里
来源:17golang原创
时间:2026-09-07 19:21:15 167浏览 收藏
Go 的 log/slog 中,logger.With("request_id", id) 不是把字段直接写进缓冲区,而是让 Logger 创建一个带新 Handler 的副本。标准 TextHandler 会在后续处理记录时输出这些属性;如果消息出现但 request_id 不见了,优先排查 Handler 链路和属性改写规则,不要先改日志格式。
- 标准 TextHandler 的 WithAttrs 会保留属性,Info 记录应能看到 request_id。
- 日志完全不出现,先看 HandlerOptions.Level;只有字段消失,再看 ReplaceAttr 和自定义 Handler。
- 包装 Handler 必须返回 inner.WithAttrs(attrs) 的新实例,不能原样返回接收者。
先用标准 TextHandler 划清问题边界
先把业务 Handler 换成标准实现,并用固定的 Info 级别打印一行。如果这段输出包含 request_id=abc-42,说明 slog 的属性继承没有问题,故障在原来的包装器、过滤器或调用路径中。
package main
import (
"context"
"os"
"log/slog"
)
func main() {
// 标准 TextHandler 会接收 Logger.With 传入的属性。
handler := slog.NewTextHandler(os.Stdout, nil)
logger := slog.New(handler).With("request_id", "abc-42")
// 使用 Info,避免默认 LevelInfo 把 Debug 记录提前过滤掉。
logger.InfoContext(context.Background(), "开始处理请求")
}
预期文本至少包含 level=INFO msg="开始处理请求" request_id=abc-42。如果整行没有输出,把问题归到级别过滤;如果整行存在但字段消失,继续看属性改写和 Handler 包装。

日志不出现和字段不出现是两种故障
HandlerOptions.Level 控制记录是否被处理。它不会只隐藏 request_id,而是让低于最低级别的整条记录都不进入 Handle。因此把最低级别设为 Warn 后,调用 logger.Info 看不到任何输出是正常现象。
另一条常见路径是 ReplaceAttr。它会在属性写出前接收已经解析的属性;如果针对 request_id 返回零值 slog.Attr{},这个字段就会被明确丢弃。调试时先临时移除 ReplaceAttr,再决定是否需要按分组路径过滤。
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
// 只隐藏密码,不要把所有业务属性误删。
if attr.Key == "password" {
return slog.Attr{}
}
return attr
},
}
logger := slog.New(slog.NewTextHandler(os.Stdout, opts)).With("request_id", "abc-42")
logger.Info("登录检查")
如果回调里写了“非白名单都返回零值”,或只判断 groups 而忽略了属性键,WithAttrs 产生的字段就会被过滤掉。分组日志还要注意:WithGroup("request") 后,回调看到的路径会包含 request,不能继续按顶层字段名猜测。
自定义 Handler 必须复制属性继承关系
很多项目会用一个轻量包装器增加采样、路由或脱敏。这里最容易出现“消息有了,WithAttrs 没了”:包装器的 WithAttrs 直接返回了自己,或者创建了新包装器却把原始 inner 放了进去。
type routeHandler struct {
inner slog.Handler
}
func (h *routeHandler) Enabled(ctx context.Context, level slog.Level) bool {
// 级别判断交给内层 Handler,保持原有过滤策略。
return h.inner.Enabled(ctx, level)
}
func (h *routeHandler) Handle(ctx context.Context, record slog.Record) error {
// 这里可以做路由,但不要重新构造时丢掉 record 的属性。
return h.inner.Handle(ctx, record)
}
func (h *routeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
// 必须把属性继续交给内层,并返回新的包装器。
return &routeHandler{inner: h.inner.WithAttrs(attrs)}
}
func (h *routeHandler) WithGroup(name string) slog.Handler {
// 分组也要沿同一条 Handler 链传递。
return &routeHandler{inner: h.inner.WithGroup(name)}
}
修复后不要直接调用 Handler 的方法做业务日志,仍然用 slog.New(h).With(...).Info(...),这样可以同时验证 Logger 副本、级别判断和 Handler 的属性继承。
按这张清单定位实际输出路径
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
| 整行没有 | Level、Enabled、是否用了 Debug | 先换 Info 或降低 Level |
| 消息有,WithAttrs 没有 | 自定义 Handler.WithAttrs | 返回 inner.WithAttrs(attrs) 的新包装器 |
| 只有某些键没有 | ReplaceAttr、脱敏白名单 | 保留非目标键,检查 groups |
| 分组字段位置异常 | WithGroup 调用顺序 | 统一 logger.WithGroup 后再 With |
| 改了代码却无变化 | 实际使用的 Logger 实例 | 避免混用默认 Logger 和局部 Logger |

常见问题
WithAttrs 会影响 Handler.Enabled 吗?
不会。Enabled 负责判断某个级别是否处理,WithAttrs 负责构造带属性的 Handler;低级别记录会在属性处理前被整体跳过。
为什么把 Debug 改成 Info 后字段就出现了?
默认 TextHandler 的最低级别是 Info。Debug 记录被过滤时,字段并非单独丢失,而是整条记录没有进入 Handle。
WithGroup 后 ReplaceAttr 应该判断什么?
同时看 groups 和 attr.Key。同一个键在不同分组下可能代表不同字段,不能只用键名做全局删除。
标准 Handler 正常,自定义 Handler 该怎么回滚?
先临时换回标准 TextHandler 保证日志可见,再逐个恢复包装器;每恢复一层都用同一个 Info 记录检查 request_id 是否仍在。
排查这类问题时,先问“整条记录是否被处理”,再问“属性在哪一层被改写”。标准实现能输出字段,就说明重点不在文本格式,而在 Handler 链路、过滤规则和实际 Logger 实例。
-
207 收藏
-
143 收藏
-
194 收藏
-
219 收藏
-
247 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习