Go slog LogValuer 递归引用为什么会被替换成错误值
来源:17golang原创
时间:2026-09-28 04:50:25 158浏览 收藏
LogValuer 递归引用时,slog 并不会把它悄悄替换成某个业务默认值。Value.Resolve() 会反复调用 LogValue;如果一直没有得到终值,超过内部调用上限后返回一个包含错误的 Value。因此排查重点是:返回链是否能收敛、是否把 Group 当成已经递归展开,以及自定义 Handler 有没有先 Resolve。
最稳妥的修复是让每个
LogValue最终返回普通值或有限的 Group,并在 Handler 中通过Resolve后检查Kind;不要在方法里直接返回自己或互相返回。
Resolve处理的是当前 Value 的 LogValuer 链,官方实现最多尝试 100 次。- 递归上限触发后得到的是 error Value;LogValue panic 也会被转成错误值。
- Resolve 得到 Group 后不会替组内每个 Attr 再递归求值,Handler 需要明确自己的遍历策略。
先看清 Resolve 的递归边界
一个常见误区是把“递归引用”理解成 Go 运行时立刻栈溢出。slog 的入口通常是 Handler 调用 Resolve,它先判断当前值是不是 KindLogValuer,再调用一次 LogValue,如此循环。正常链路应当在某一层返回字符串、数字、时间或 Group;如果 A 返回 B,B 又返回 A,就没有稳定终点。

官方源码把调用次数上限定义为 100。达到上限后,返回值的底层内容是一个错误,错误文本会指出 LogValue called too many times 以及原始值类型。这个结果的含义是“求值没有收敛”,不是把原对象转换成了错误字符串。
三类写法要分开判断
第一类是有限包装:外层只返回一次 slog.String 或 slog.Group,这是正常用法。第二类是自引用,方法直接返回自己;第三类是互引用,A 返回 B、B 返回 A。后两类都应当修正数据模型或返回逻辑,而不是把 100 次当成可配置的业务重试次数。
type traceValue struct {
ID string
}
func (v traceValue) LogValue() slog.Value {
// 终止在普通字符串,避免返回 slog.Any("trace", v) 形成自引用。
return slog.String("trace_id", v.ID)
}
type badValue struct{}
func (badValue) LogValue() slog.Value {
// 错误示例:返回自身会让 Resolve 一直重复调用 LogValue。
return slog.AnyValue(badValue{})
}
生产代码里还要注意接收者和值的类型。把一个指针包装进 slog.Any 后,它是否实现 LogValuer 取决于方法集;排查时可以打印 v.Kind(),不要只看 fmt.Sprint(v.Any()) 的外观。
Handler 里怎样处理替换结果
自定义 Handler 不应直接调用 attr.Value.LogValuer().LogValue() 并假设一次就结束。先 Resolve,才能获得官方定义的递归保护;随后按 Kind 处理普通值、Group 和错误值。Group 是一个边界:Resolve 返回 Group 后,组内 Attr 不会自动逐个递归 Resolve,是否遍历以及如何避免重复展开由 Handler 自己负责。

func resolvedText(a slog.Attr) (string, error) {
v := a.Value.Resolve()
// KindAny 可能承载 Resolve 产生的 error,先取出再决定如何记录。
if v.Kind() == slog.KindAny {
if err, ok := v.Any().(error); ok {
return "", err
}
}
if v.Kind() == slog.KindGroup {
// Group 的成员是否继续 Resolve,要由遍历代码显式决定。
return "group", nil
}
return v.String(), nil
}
如果这是日志链路,建议把错误值作为结构化字段记录,例如 resolve_error,同时保留原 Attr 的 key,便于定位是哪一个字段的 LogValue 没有收敛。不要在错误分支再次把同一个 Value 交给 Resolve,否则只会重复消耗排查时间。
用四组测试锁住边界
单元测试至少覆盖:一次返回字符串的正常值、自引用、互引用和 LogValue panic。测试不必依赖完整 JSON 日志;直接调用 slog.AnyValue(x).Resolve(),检查终值 Kind 或 Any() 中的 error,更容易把语义和 Handler 输出分开。
func TestResolveBoundary(t *testing.T) {
cases := []struct {
name string
value any
wantErr bool
}{
{"stable", traceValue{ID: "t-1"}, false},
{"self-reference", badValue{}, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := slog.AnyValue(tc.value).Resolve()
// 错误值只验证可识别的 error,不绑定整段运行时文本。
_, isErr := got.Any().(error)
if isErr != tc.wantErr {
t.Fatalf("error=%v, want %v", isErr, tc.wantErr)
}
})
}
}
实际项目再补一组互引用和 panic 测试,并把 Group 的成员策略单独测出来。这样升级 Go 或更换 Handler 时,失败会明确落在“求值没有收敛”还是“组内遍历重复”上。
相关问题与处理结论
把 100 次上限调大能解决问题吗?
不能。它只是防止无限循环,不能让自引用变成正确的业务值。应修改 LogValue 返回链,让它在有限次数内结束。
返回 Group 后为什么还有 LogValuer 没展开?
这是 Resolve 的边界设计:Group 本身已经解析完成,但组内属性不会由该次调用自动递归展开。需要展开时,在 Handler 的遍历逻辑中逐个处理并设置清晰的停止条件。
收尾检查
看到“错误值”时先确认它来自递归上限还是 panic,再检查 LogValue 是否返回自身、互相返回或把仍实现 LogValuer 的包装继续传下去。最后确认自定义 Handler 调用了 Resolve,并为普通值、Group 和错误值分别保留可检索的字段。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
260 收藏
-
193 收藏
-
405 收藏
-
156 收藏
-
427 收藏
-
433 收藏
-
283 收藏
-
298 收藏
-
238 收藏
-
470 收藏
-
384 收藏
-
454 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习