当前位置:首页 > 文章列表 > Golang > Go问答 > Go slog LogValuer 递归引用为什么会被替换成错误值

Go slog LogValuer 递归引用为什么会被替换成错误值

来源:17golang原创 2026-09-28 04:50:25 0浏览 收藏

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,就没有稳定终点。

Go slog Value.Resolve 处理 LogValuer A、LogValuer B、终值和 error Value 的递归边界说明图

官方源码把调用次数上限定义为 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 自己负责。

自定义 Handler 先 Resolve 再按 KindGroup、普通值和 error Value 分流的 Go slog 处理边界图
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 和错误值分别保留可检索的字段。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
出口企业申请原产地证前要准备什么出口企业申请原产地证前要准备什么
上一篇
出口企业申请原产地证前要准备什么
磨砂花瓣微距手机壁纸提示词怎么写
下一篇
磨砂花瓣微距手机壁纸提示词怎么写
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    247次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    293次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    262次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    243次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    51次使用