当前位置:首页 > 文章列表 > Golang > Go教程 > Go JSON 字段名称不固定时怎么用 RawMessage 分层解析

Go JSON 字段名称不固定时怎么用 RawMessage 分层解析

来源:17golang原创 2026-09-07 07:35:48 0浏览 收藏

接口返回的 JSON 看起来只有一个 kind 和一个 payload,但 payload 可能是创建事件,也可能是删除事件。此时不要先把所有字段堆进一个“大而全”的结构体:用 json.RawMessage 先保留内部 JSON,读取判别字段后再做第二次解码,结构更清楚,错误也能落到正确层级。

外层结构稳定就先解外层;内部结构由 kind 决定时,把 payload 声明为 json.RawMessage,在分支里解码成具体类型。
要点速览
  • RawMessage 适合“延迟决定目标类型”,不是任意 JSON 的自动推断器。
  • 外层先处理语法错误,分支处理未知 kind,具体结构体负责字段类型错误。
  • 二次解码前要检查空载荷,生产代码还应决定未知事件是拒绝、记录还是兼容放行。

为什么不能直接用一个大结构体接住

假设事件有两种形态:

{"kind":"created","payload":{"id":101,"name":"日报"}}
{"kind":"deleted","payload":{"id":101,"reason":"重复数据"}}

两种 payload 的字段集合不同。直接使用 map[string]any 虽然能接住数据,却把数字、字符串和缺失字段的判断推迟到了业务代码里;把所有可能字段放入一个结构体,又很难区分“该事件不拥有这个字段”和“字段应该存在但接口漏了”。更稳妥的边界是:Envelope 只关心 kind 与原始 payload,具体事件结构体各自负责自己的字段。

先解外层,再决定内部类型

下面的写法把“判别”和“取值”分开。json.Unmarshal 先将 payload 保存在 RawMessage 中,DecodeEvent 再根据 kind 选择目标结构。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

type envelope struct {
    Kind    string          `json:"kind"`
    Payload json.RawMessage `json:"payload"`
}

type createdPayload struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type deletedPayload struct {
    ID     int    `json:"id"`
    Reason string `json:"reason"`
}

func decodeEvent(data []byte) (any, error) {
    var outer envelope
    // 第一层只读取判别字段和原始载荷,避免猜测内部结构。
    if err := json.Unmarshal(data, &outer); err != nil {
        return nil, fmt.Errorf("decode envelope: %w", err)
    }
    // 空载荷不能进入二次解码,否则错误信息会掩盖真正的问题。
    if len(bytes.TrimSpace(outer.Payload)) == 0 || bytes.Equal(bytes.TrimSpace(outer.Payload), []byte("null")) {
        return nil, fmt.Errorf("kind %q has empty payload", outer.Kind)
    }

    switch outer.Kind {
    case "created":
        var value createdPayload
        // kind 已确定,RawMessage 此时才交给具体结构体。
        if err := json.Unmarshal(outer.Payload, &value); err != nil {
            return nil, fmt.Errorf("decode created payload: %w", err)
        }
        return value, nil
    case "deleted":
        var value deletedPayload
        // 删除事件使用自己的字段边界,不复用创建事件结构。
        if err := json.Unmarshal(outer.Payload, &value); err != nil {
            return nil, fmt.Errorf("decode deleted payload: %w", err)
        }
        return value, nil
    default:
        // 未知 kind 由分派层明确拒绝,便于上层决定告警或兼容策略。
        return nil, fmt.Errorf("unknown event kind %q", outer.Kind)
    }
}

这段函数返回 any 是为了突出分层过程;真实项目也可以返回统一接口,例如让 createdPayloaddeletedPayload 都实现 Event。关键不在返回类型,而在于第二次 Unmarshal 发生在类型选择之后。

Go JSON RawMessage 分层解析图:原始 JSON、Envelope、Kind 字段、Payload RawMessage 与两个业务载荷的静态关系
图1:固定信封只承载 Kind 与 Payload,RawMessage 把内部 JSON 保留到具体类型确定之后。

字段类型错误应该在哪一层返回

分层解析的价值还在错误边界。JSON 少一个逗号,属于外层语法问题; kind 不是服务端认识的值,属于分派问题;id 被传成无法转换的字符串,则属于具体 payload 的字段问题。不要把三种错误都改写成“JSON 解析失败”,否则排查时无法判断该修协议、加分支还是修数据。

现象处理层建议动作
外层 JSON 不合法第一次 Unmarshal返回原始语法错误并记录请求边界
kind 未登记switch 分派明确拒绝或进入兼容策略
payload 为 null 或空白二次解码前返回缺失载荷错误
内部字段类型不符具体结构体 Unmarshal保留字段上下文并修正数据契约

如果协议允许未来新增事件,未知 kind 不一定必须让整个消费循环退出。可以返回带 kind 的错误,让上层选择记录后跳过;但不要静默把未知 payload 当作已知结构体,否则后续字段丢失会变成更隐蔽的数据问题。

Go JSON RawMessage 错误边界图:JSON 字节、外层解码、Kind 分支与未知类型空载荷字段错误的分层关系
图2:异常应在对应层处理;RawMessage 延迟了解码,但不会替业务代码自动消化未知类型或字段错误。

RawMessage 的边界和常见误区

第一,RawMessage 不是验证器。它让你暂时保留一段原始 JSON,真正的字段约束仍由第二次解码和业务校验完成。第二,不能只判断 kind 就假设 payload 一定完整;空值、数组、字符串都可能与预期结构不符。第三,如果判别字段本身也不固定,就应先定义协议层的识别规则,不能靠遍历 map 的第一个 key 猜类型。

还要注意返回值的所有权。示例中第二次解码直接从 outer.Payload 读取;如果要把原始片段长期保存,应在自己的数据结构中复制需要的字节,而不是把 RawMessage 当成自动管理的业务对象。对外部输入来说,错误信息应带上 kind 和事件来源,但不要把整段敏感 payload 写进日志。

延伸问答:什么时候继续用 RawMessage

可以直接用 map[string]any 吗?

可以处理探索性数据,但长期接口最好让具体结构体承担类型约束。map 更适合字段真的完全开放、且业务只做透传的场景。

RawMessage 能自动识别多个版本吗?

不能。它只保留原始 JSON;版本或 kind 的识别字段仍需要由协议明确提供,再由代码选择对应结构。

什么时候应该自定义 UnmarshalJSON?

当某个类型自身就有稳定的多形态解析规则,并且希望所有调用方共享这套规则时,可以封装到自定义 UnmarshalJSON。如果只是一个接口的少数分支,外层信封加 RawMessage 往往更直观。

把 JSON 解析拆成“外层信封、判别字段、二次载荷”三段后,字段不固定就不再意味着到处写断言。先定义边界,再决定未知类型的策略,通常比继续扩张一个大结构体更容易维护。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python pathlib glob 找不到隐藏文件时怎么处理Python pathlib glob 找不到隐藏文件时怎么处理
上一篇
Python pathlib glob 找不到隐藏文件时怎么处理
Linux cgroups v2 怎么判断容器内存限制是否生效
下一篇
Linux cgroups v2 怎么判断容器内存限制是否生效
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    170次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    101次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    19次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    32次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    71次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码