Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段为什么结果为空或不符合预期
当一个 JSON 接口的 payload 会随着 kind 变化时,直接把它声明成 any 往往会得到 map[string]any、[]any 和浮点数等不够稳定的结果。更适合的做法是先用 json.RawMessage 接住原始 JSON 字节,等外层字段解析完成后,再按照 kind 选择具体结构体进行第二次解析。
结果为空通常不是 RawMessage 没有工作,而是字段没有映射到、输入真的缺失、输入是显式 null,或者二次解析时选择了错误的目标类型。下面把这几种状态拆开,并给出一套可复用的写法。
官方资料:https://pkg.go.dev/encoding/json
先确认 RawMessage 字段确实被映射
json.RawMessage 的本质是保存一段原始 JSON 编码的字节。外层解码时,稳定字段可以直接落到结构体,结构不稳定的字段则先保留在 Payload 中。字段必须是导出的,并且建议用明确的 json 标签把外部字段名固定下来:
package main
import (
"encoding/json"
"fmt"
)
type Envelope struct {
Kind string `json:"kind"`
Request string `json:"request"`
Payload json.RawMessage `json:"payload"`
}
func main() {
input := []byte(`{"kind":"user","request":"r-17","payload":{"id":7,"name":"小林"}}`)
var envelope Envelope
if err := json.Unmarshal(input, &envelope); err != nil {
// 外层 JSON 损坏时,先返回外层错误,不要继续猜 payload 的结构。
fmt.Println("外层解析失败:", err)
return
}
// RawMessage 仍然保存 payload 的原始 JSON,稍后再决定目标类型。
fmt.Printf("kind=%s request=%s payload=%s\n", envelope.Kind, envelope.Request, envelope.Payload)
}
如果 Payload 是小写字段,或者标签写成了与输入不同的字段名,它就可能保持零值。对 RawMessage 来说,零值通常表现为 nil 或长度为 0;这和输入里明确写了 "payload":null 不是同一种状态。

按 kind 保留原始字节并选择目标结构
实际接口通常有一个稳定的路由字段,例如 kind。先解析 kind,再把同一段 RawMessage 解码到不同结构体,可以避免把不确定数据过早降级为通用 map。
package main
import (
"encoding/json"
"fmt"
)
type Envelope struct {
Kind string `json:"kind"`
Payload json.RawMessage `json:"payload"`
}
type UserPayload struct {
ID int `json:"id"`
Name string `json:"name"`
}
type MetricPayload struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
func decodePayload(input []byte) (any, error) {
var envelope Envelope
if err := json.Unmarshal(input, &envelope); err != nil {
// 外层结构失败时,错误上下文应保留在外层。
return nil, fmt.Errorf("解析 envelope: %w", err)
}
if len(envelope.Payload) == 0 {
// 字段缺失时不进入具体结构体,避免把零值误当业务数据。
return nil, fmt.Errorf("kind=%q 缺少 payload", envelope.Kind)
}
switch envelope.Kind {
case "user":
var payload UserPayload
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
// kind 与 payload 形状不一致时,返回二次解析错误。
return nil, fmt.Errorf("解析 user payload: %w", err)
}
return payload, nil
case "metric":
var payload MetricPayload
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
// 不要吞掉类型错误,否则调用方只会看到一个空结构体。
return nil, fmt.Errorf("解析 metric payload: %w", err)
}
return payload, nil
default:
// 未知 kind 先拒绝,比静默返回 map 更容易发现协议变更。
return nil, fmt.Errorf("未知 kind=%q", envelope.Kind)
}
}
func main() {
input := []byte(`{"kind":"metric","payload":{"value":12.5,"unit":"ms"}}`)
value, err := decodePayload(input)
if err != nil {
fmt.Println("解析失败:", err)
return
}
fmt.Printf("%T: %+v\n", value, value)
}
这里的关键顺序是“外层一次、分支一次”。RawMessage 不会自动替你判断目标结构,也不会因为 kind 改变就自动转换;它只把选择权留到业务代码中。若服务端给出的字段形状可能变化,建议让每个分支都返回带上下文的错误。
区分缺失、null、合法对象和错误形状
排查“结果为空”时,先不要直接打印结构体。应同时观察原始字段的长度和内容,因为四种输入会得到不同结果:
| 输入状态 | RawMessage 表现 | 后续处理 |
|---|---|---|
| 没有 payload 字段 | 通常为 nil,长度为 0 | 按协议缺字段处理 |
"payload":null | 包含字节 null | 按业务约定接受或拒绝 null |
| 合法对象或数组 | 保留完整原始 JSON | 选择匹配的目标类型再 Unmarshal |
| 目标类型不匹配 | 原始字节仍然存在 | 二次解析返回类型错误,不应忽略 |
特别要注意:空对象 {} 不是字段缺失。它可能成功解码成一个字段全是零值的结构体;如果业务需要区分“没有数据”和“数据为空”,要在结构体之外保留输入状态,不能只看二次解析后的字段值。

为什么二次解析后字段还是空
如果 RawMessage 有内容,但目标结构体字段仍是零值,优先按下面顺序排查:
- 目标字段是否导出。 Go 的 JSON 解码只能写入可设置的导出字段,字段名小写时不会得到输入值。
- 标签是否对应实际 JSON。 比如输入是
user_id,目标字段却没有json:"user_id"标签,字段映射就可能不符合预期。 - kind 与结构是否配套。 把数组交给对象结构体,或者把字符串交给数值字段,二次解析会返回错误;不要只打印目标结构体。
- 是否复用了旧变量。 在循环中重复 Unmarshal 到同一个非零结构体时,容易把前一次状态带进下一次判断。每次分支都创建新的目标变量,或显式清空它。
- 是否把 null 当作空对象。 对指针、切片和 map 等目标类型,
null可能表现为 nil;这代表输入是 null,而不是解析失败。
可以给二次解析加一层明确的错误包装,让日志同时带上 kind 和目标分支。这样看到“字段为空”时,先能确认是没有进入分支、输入为空,还是目标类型不匹配。
把 RawMessage 解析路径固定成检查清单
这类代码适合集中在一个小函数中处理,调用方只接收具体类型和错误。发布或改接口前,可以按下面的清单复查:
- 外层结构体字段是导出的,并且对不稳定字段使用了明确的
json:"payload"标签。 - 先检查
len(raw)判断字段是否缺失,再决定是否允许显式null。 - 使用稳定的
kind或类似路由字段选择目标结构,而不是猜测 JSON 形状。 - 每次二次
json.Unmarshal都检查错误,并将目标分支写入错误上下文。 - 空对象、空数组、
null和字段缺失有不同业务含义时,保留原始状态。 - 服务端新增
kind时让默认分支显式报错,避免未知数据悄悄变成空结构。
可以把核心原则记成一句话:RawMessage 负责延迟决定,kind 负责选择目标,Unmarshal 错误负责说明不匹配。 只要把这三个职责分开,结果为空的问题通常都能落到具体输入状态,而不是停留在“JSON 没解析出来”的模糊结论上。
相关问题
字段缺失和 payload 为 null 怎么区分?
字段缺失时 RawMessage 通常是 nil 或长度为 0;显式 null 会保留 null 这几个字节。先检查长度,再根据原始内容判断是否为 null。
可以先把 RawMessage 转成 map[string]any 吗?
可以,但这样会失去具体结构体的类型约束,数字等值也可能落成通用类型。结构稳定或有明确 kind 时,优先直接二次解析到目标结构体。
二次解析失败时应该返回空结构体吗?
不建议。空结构体可能被误当成成功结果,应返回带 kind 和目标类型上下文的错误,让调用方区分协议错误与合法零值。
Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段怎样完成一个可复用的最小示例
- 上一篇
- Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段怎样完成一个可复用的最小示例
- 下一篇
- Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段怎样在批处理代码中减少额外分配
-
- Golang · Go问答 | 25分钟前 |
- Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段怎样完成一个可复用的最小示例
- 121浏览 收藏
-
- Golang · Go问答 | 28分钟前 |
- Go问答:bufio.Scanner读取长行为什么提前停止怎样安全扩展缓冲区
- 362浏览 收藏
-
- Golang · Go问答 | 31分钟前 |
- Go问答:http.Transport 的 TLSClientConfig 未设置 ServerName 为什么握手失败
- 271浏览 收藏
-
- Golang · Go问答 | 34分钟前 | 标准库 · 错误处理 · Go问答 · JSON Go HTTP接口 DisallowUnknownFields json.Decoder 未知字段
- Go问答:json.Decoder DisallowUnknownFields接口收到多余字段时怎样提前报错怎样完成一个可复用的最小示例
- 323浏览 收藏
-
- Golang · Go问答 | 37分钟前 |
- Go问答:url.PathEscape路径参数为什么被错误解码怎样在批处理代码中减少额外分配
- 111浏览 收藏
-
- Golang · Go问答 | 40分钟前 |
- Go问答:url.PathEscape路径参数为什么被错误解码为什么结果为空或不符合预期
- 123浏览 收藏
-
- Golang · Go问答 | 43分钟前 | Go问答 · 接口排查 · Go URL编码 net/url 路径参数 url.PathEscape PathUnescape
- Go问答:url.PathEscape路径参数为什么被错误解码怎样完成一个可复用的最小示例
- 285浏览 收藏
-
- Golang · Go问答 | 46分钟前 |
- Go问答:net.ListenConfig监听端口怎样设置底层控制参数怎样在批处理代码中减少额外分配
- 143浏览 收藏
-
- Golang · Go问答 | 49分钟前 |
- Go问答:net.ListenConfig监听端口怎样设置底层控制参数为什么结果为空或不符合预期
- 327浏览 收藏
-
- Golang · Go问答 | 52分钟前 |
- Go问答:net.ListenConfig监听端口怎样设置底层控制参数怎样完成一个可复用的最小示例
- 150浏览 收藏
-
- Golang · Go问答 | 58分钟前 | 故障排查 · Go keepalive 网络排查 TCP长连接 net.Dialer
- Go问答:net.Dialer KeepAlive长连接空闲后为什么频繁断开为什么结果为空或不符合预期
- 256浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 961次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 964次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1076次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1052次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 884次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go flag.FlagSet ContinueOnError 怎么捕获帮助输出和解析错误
- 2026-11-05 501浏览
-
- Go netip.ParseAddr 为什么拒绝带端口字符串
- 2026-11-03 501浏览
-
- Go generate 未执行指令的注释与目录检查
- 2026-10-28 501浏览

