当前位置:首页 > 文章列表 > Golang > Go问答 > Go slog JSON 日志为什么混入不可序列化字段

Go slog JSON 日志为什么混入不可序列化字段

来源:17golang原创 2026-10-23 03:34:29 0浏览 收藏

我第一次看到这类日志时,以为是采集器把 JSON 改坏了:某个字段明明传入了对象,最终却变成了 !ERROR:json: unsupported type: chan int。真正的原因通常在应用进程内:slog.JSONHandler 会把 Any 值交给 encoding/json,而 channel、函数、复数等值不能直接编码。编码失败时,Handler 不返回错误,而是把错误文本写回当前字段。

看到 !ERROR:json,先不要查日志平台。先检查这个字段及其嵌套值是否包含 JSON 不支持的运行时对象;修复方式是记录稳定摘要,而不是把整个对象原样塞进 slog.Any。

本文只讨论 Go 标准库 log/slog 的 JSON 输出边界,不讨论采集器、索引模板或日志平台的二次加工。

为什么日志字段会变成 !ERROR:json

JSONHandler 的普通属性会按 encoding/json.Encoder 编码。顶层 Attr 如果直接放入一个 error,Handler 会调用它的 Error 方法;但嵌套在 map、slice 或结构体里的值,仍由 JSON 编码器整体处理。

slog Any、JSONHandler、encoding/json 和 !ERROR 字符串之间的编码边界说明图
图1:slog JSON 编码边界说明图,展示不可序列化值如何落入 !ERROR 字符串。这是静态说明图,不是截图或运行证据。

例如,下面把一个 channel 藏进 map。问题不在 map 本身,而在 map 的值树里出现了 chan int:

package main

import (
    "bytes"
    "log/slog"
    "os"
)

func main() {
    // 用 JSONHandler 观察 Any 值的编码边界;channel 不能被 encoding/json 编码。
    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))
    logger.Info("request finished", "payload", map[string]any{
        "request_id": "req-17",
        "stream":     make(chan int),
    })

    // 这里把说明结果写到标准输出;日志内容仍是一条 JSON 记录。
    _, _ = os.Stdout.Write(buf.Bytes())
}
{"level":"INFO","msg":"request finished","payload":"!ERROR:json: unsupported type: chan int"}

这也解释了两个容易误判的现象:第一,整条日志通常还在,只有当前字段被替换;第二,调用方感知不到一个普通的编码返回错误,因为 JSONHandler.Handle 会把编码失败格式化为字符串后继续写出。

先区分三种值,定位会快很多

排查时我会把字段拆成三类,而不是笼统地说“JSON 有问题”。

值的来源典型写法处理结果
可直接编码的值slog.String、数字、布尔、普通结构体按 JSON 值输出
顶层 error Attrslog.Any("err", err)调用 Error(),输出字符串
嵌套不可编码值map、slice、结构体里含 channel 或 func当前字段变为 !ERROR:json: ...

因此,看到这类字符串时,先回到产生该字段的 slog.Any 或 Logger.With 调用,沿着 map、slice 和结构体继续找,不要只盯着字段名。

把运行时对象改成稳定摘要

最小修正不是把值强行转成字符串,而是定义日志需要的字段。连接、channel、函数、锁或请求对象往往带有运行时状态,直接序列化既可能失败,也可能泄露不该进入日志的内容。

运行时对象经过 LogValuer 变成稳定摘要后交给 JSONHandler 的结构图
图2:可序列化日志边界结构图,展示 LogValuer 如何只输出稳定摘要字段。这是静态说明图,不是截图或运行证据。

如果一个类型经常被记录,可以让它实现 slog.LogValuer。下面只暴露状态和容量,不把内部 channel 写进日志:

package main

import "log/slog"

type workerPool struct {
    name string
    jobs chan int
}

// LogValue 把运行时对象收敛为稳定字段,避免把 channel 传给 JSONHandler。
func (p workerPool) LogValue() slog.Value {
    // 只输出读者排障真正需要的摘要,不泄露内部任务内容。
    return slog.GroupValue(
        slog.String("name", p.name),
        slog.Int("capacity", cap(p.jobs)),
    )
}

func logPool(logger *slog.Logger, pool workerPool) {
    // LogValuer 会在 Handler 处理该属性时解析为可编码的 group。
    logger.Info("worker pool ready", slog.Any("pool", pool))
}

如果对象只使用一次,也可以在调用点显式构造 slog.Group。关键是把“日志模型”和“运行时对象”分开:

// 只记录稳定摘要;不要把 pool.jobs 原样放入 slog.Any。
logger.Info("worker pool ready",
    slog.Group("pool",
        slog.String("name", pool.name),
        slog.Int("capacity", cap(pool.jobs)),
    ),
)

修复后怎么判断已经处理对了

复查时我只看日志字段的契约,不追求把对象“完整打印出来”。可以按下面三点检查:

  1. 同一种对象在不同调用点输出相同的字段名和类型,不能一处是对象、一处是长字符串。
  2. 字段树里不再包含 channel、func、不可控的自定义 Marshaler 等运行时值;必要信息已经转换为字符串、数字或 group。
  3. 敏感字段在 LogValue 或显式 group 中被主动省略,不能因为“方便排查”把整个请求对象带进日志。

如果仍出现 !ERROR:json,继续沿着被替换的字段向下拆分。尤其要注意:一个顶层结构体即使看起来普通,只要某个导出字段或自定义 MarshalJSON 返回错误,整个该字段仍可能触发 JSONHandler 的兜底文本。

常见问题

为什么直接记录 error 通常没有这个问题?

因为 JSONHandler 对作为 Attr 值的 error 有专门处理,会调用 Error() 输出字符串。但 error 如果嵌在 map、slice 或结构体内部,就回到 encoding/json 的普通规则,不能把这个特例扩大到整个对象树。

能不能用 fmt.Sprint 统一解决?

可以临时止血,但会丢掉结构化字段,也可能把内部状态和敏感内容一股脑写进日志。长期方案优先使用显式 slog.Group 或 LogValuer,让字段集合成为稳定契约。

为什么不直接让 Handler 返回错误?

这是内置 JSONHandler 的设计:单条记录的某个值编码失败时,Handler 用 !ERROR: 字符串保留这条日志。应用如果需要严格失败策略,应在进入 slog 前自行构造可编码的日志 DTO,而不是依赖 Handler 作为数据校验器。

总结一下:!ERROR:json 不是日志平台凭空混入的字段,而是 slog.JSONHandler 对编码失败的可见兜底。找到产生 slog.Any 的对象,拆掉不可序列化运行时值,再用稳定摘要或 LogValuer 输出,问题就会从“日志偶尔变形”变成可维护的字段契约。

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