Go JSON 字段名称不固定时怎么用 RawMessage 分层解析
来源:17golang原创
时间:2026-09-07 07:35:48 340浏览 收藏
接口返回的 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 是为了突出分层过程;真实项目也可以返回统一接口,例如让 createdPayload 和 deletedPayload 都实现 Event。关键不在返回类型,而在于第二次 Unmarshal 发生在类型选择之后。

字段类型错误应该在哪一层返回
分层解析的价值还在错误边界。JSON 少一个逗号,属于外层语法问题; kind 不是服务端认识的值,属于分派问题;id 被传成无法转换的字符串,则属于具体 payload 的字段问题。不要把三种错误都改写成“JSON 解析失败”,否则排查时无法判断该修协议、加分支还是修数据。
| 现象 | 处理层 | 建议动作 |
|---|---|---|
| 外层 JSON 不合法 | 第一次 Unmarshal | 返回原始语法错误并记录请求边界 |
| kind 未登记 | switch 分派 | 明确拒绝或进入兼容策略 |
| payload 为 null 或空白 | 二次解码前 | 返回缺失载荷错误 |
| 内部字段类型不符 | 具体结构体 Unmarshal | 保留字段上下文并修正数据契约 |
如果协议允许未来新增事件,未知 kind 不一定必须让整个消费循环退出。可以返回带 kind 的错误,让上层选择记录后跳过;但不要静默把未知 payload 当作已知结构体,否则后续字段丢失会变成更隐蔽的数据问题。

RawMessage 的边界和常见误区
第一,RawMessage 不是验证器。它让你暂时保留一段原始 JSON,真正的字段约束仍由第二次解码和业务校验完成。第二,不能只判断 kind 就假设 payload 一定完整;空值、数组、字符串都可能与预期结构不符。第三,如果判别字段本身也不固定,就应先定义协议层的识别规则,不能靠遍历 map 的第一个 key 猜类型。
还要注意返回值的所有权。示例中第二次解码直接从 outer.Payload 读取;如果要把原始片段长期保存,应在自己的数据结构中复制需要的字节,而不是把 RawMessage 当成自动管理的业务对象。对外部输入来说,错误信息应带上 kind 和事件来源,但不要把整段敏感 payload 写进日志。
延伸问答:什么时候继续用 RawMessage
可以直接用 map[string]any 吗?
可以处理探索性数据,但长期接口最好让具体结构体承担类型约束。map 更适合字段真的完全开放、且业务只做透传的场景。
RawMessage 能自动识别多个版本吗?
不能。它只保留原始 JSON;版本或 kind 的识别字段仍需要由协议明确提供,再由代码选择对应结构。
什么时候应该自定义 UnmarshalJSON?
当某个类型自身就有稳定的多形态解析规则,并且希望所有调用方共享这套规则时,可以封装到自定义 UnmarshalJSON。如果只是一个接口的少数分支,外层信封加 RawMessage 往往更直观。
把 JSON 解析拆成“外层信封、判别字段、二次载荷”三段后,字段不固定就不再意味着到处写断言。先定义边界,再决定未知类型的策略,通常比继续扩张一个大结构体更容易维护。
-
190 收藏
-
128 收藏
-
355 收藏
-
447 收藏
-
152 收藏
-
281 收藏
-
343 收藏
-
344 收藏
-
180 收藏
-
282 收藏
-
240 收藏
-
479 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习