Go JSON 怎样延迟解析不同类型字段
Go JSON 中同一个字段可能是对象,也可能是数组时,可以把该字段声明为 json.RawMessage:先取得外层类型标记,再按标记把保留的 JSON 内容解码成对应类型。延迟的是具体类型绑定,不是允许输入跳过 JSON 语法检查。
我更愿意用“通知消息”和“标签消息”做一个小工具来说明这件事:同样叫 payload,一个需要 text,另一个需要字符串列表。我的取舍是让信封保持简单,把业务含义留给各自分支;如果协议只有一种固定结构,我不会为了灵活而到处加 RawMessage。
Go 官方地址:https://go.dev/
标准库文档:https://pkg.go.dev/encoding/json
我会先把消息分成信封和内容
小项目的外层类型叫 Envelope,包含 Kind 和 Payload。Kind 对应 JSON 的 kind,是协议约定的分支标记;Payload 对应 payload,使用 RawMessage 暂存完整的 JSON 值。不要假定 payload 一定是对象,它也可以是数组、字符串或其他合法值;允许哪一种由分支决定。
这与先存进 interface{} 再做类型断言不同。通用解码会得到 map、切片等动态结构,而这里暂不构造这些业务对象。RawMessage 并不替我们推断类型;“notice 使用 Notice、tags 使用 []string”仍是应用协议。
官方文档把 RawMessage 定位为保留编码后的 JSON 值、支持延迟解码的类型。本文采用 encoding/json 的传统接口,不涉及 encoding/json/v2 的迁移。RawMessage 文档

准备一个只依赖标准库的小工具
准备可正常使用标准库的 Go 开发环境,在自己的练习目录创建项目。这里只给读者复现命令,不声称文章示例已经在本机执行。
# 创建练习模块,然后把下节完整代码保存为 main.go。 mkdir raw-message-demo cd raw-message-demo go mod init example.com/raw-message-demo
我们自定两种输入:kind=notice 配一个具有非空 text 的对象;kind=tags 配一个非空字符串数组,且标签不能只含空白。所有输入只接受一条 JSON 值。64 KiB 是这个演示的输入上限,不是 RawMessage 的容量承诺。
把延迟解码写进完整的main.go
把下面内容保存为 main.go。我把严格单值解码放在 decodeOne,把业务分支放在 describe,入口 run 只承担有限读取和输出。这样可以分别讨论“读不到”“信封不对”“内容类型不对”和“业务内容为空”。
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
)
// Envelope 只固定路由字段,内容暂时保留为 JSON 片段。
type Envelope struct {
Kind string `json:"kind"`
Payload json.RawMessage `json:"payload"`
}
type Notice struct {
Text string `json:"text"`
}
// decodeOne 要求一个完整 JSON 值;结构体不接收未知字段。
func decodeOne(data []byte, dst interface{}) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
return err
}
var extra json.RawMessage
if err := dec.Decode(&extra); err != io.EOF {
if err == nil {
return errors.New("只允许一个 JSON 值")
}
return fmt.Errorf("尾部内容无效: %w", err)
}
return nil
}
func describe(data []byte) (string, error) {
// 每条消息使用新变量,避免缺失字段沿用上一条数据。
var env Envelope
if err := decodeOne(data, &env); err != nil {
return "", fmt.Errorf("信封解析失败: %w", err)
}
raw := bytes.TrimSpace(env.Payload)
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return "", errors.New("payload 必须存在且不能为 null")
}
// kind 是协议标记,不依赖字段在 JSON 中的排列次序。
switch env.Kind {
case "notice":
var n Notice
if err := decodeOne(raw, &n); err != nil {
return "", fmt.Errorf("notice 内容解析失败: %w", err)
}
if strings.TrimSpace(n.Text) == "" {
return "", errors.New("notice.text 不能为空")
}
return "notice: " + n.Text, nil
case "tags":
var tags []string
if err := decodeOne(raw, &tags); err != nil {
return "", fmt.Errorf("tags 内容解析失败: %w", err)
}
if len(tags) == 0 {
return "", errors.New("tags 至少包含一个标签")
}
for _, tag := range tags {
if strings.TrimSpace(tag) == "" {
return "", errors.New("tags 不能包含空标签")
}
}
return fmt.Sprintf("tags: %d", len(tags)), nil
default:
return "", fmt.Errorf("不支持的 kind: %q", env.Kind)
}
}
func run() error {
const maxBytes = 64 * 1024 // 示例协议的自定上限,并非标准库默认值。
data, err := io.ReadAll(io.LimitReader(os.Stdin, maxBytes+1))
if err != nil {
return fmt.Errorf("读取输入失败: %w", err)
}
if len(data) > maxBytes {
return errors.New("输入超过 64 KiB")
}
result, err := describe(data)
if err != nil {
return err
}
_, err = fmt.Fprintln(os.Stdout, result)
return err
}
func main() {
// 错误统一交给标准错误输出,不吞掉任何解码失败。
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
decodeOne 的第二次 Decode 是为了确认后面没有第二个值或无效尾部。只在第一次 Decode 上调用 DisallowUnknownFields,并不意味着 RawMessage 内部的字段也已经按业务结构检查过;进入 notice 分支后,再用新 Decoder 将内容绑定到 Notice,才会约束其中的字段。
需要注意,拒绝未知字段不等于 JSON Schema 校验;本项目另写空字符串、空数组等业务规则,也没有实现重复键拒绝。不要把这个小工具宣传为完整协议检查器。Decoder 字段规则

用两种输入看懂程序该做什么
先试通知对象,再试标签数组。下面是可复制的命令;printf 把单条输入交给标准输入,不需要安装额外依赖。
# 通知的 payload 是对象,text 是业务内容。
printf '%s\n' '{"kind":"notice","payload":{"text":"导入计划已保存"}}' | go run .
# 标签的 payload 是数组,而不是把数组编码成字符串。
printf '%s\n' '{"payload":["入门","标准库"],"kind":"tags"}' | go run .
按示例代码应分别得到以下输出,属于读者复现时的预期结果,不是运行记录:
notice: 导入计划已保存
tags: 2
第二条输入故意把 payload 放到 kind 前面。代码先解出整个 Envelope,再进入 switch,因此不依赖这两个字段在文本中的先后位置。我喜欢这一点:调用方调整序列化字段顺序,不需要改分发代码。
想观察错误路径,可以把 notice 的 payload 改成数组,或把 tags 改成字符串。字段本身虽是合法 JSON,业务分支仍应拒绝不匹配的类型。
# 故意传入错误的业务类型,应得到非零退出码。
printf '%s\n' '{"kind":"notice","payload":["错误形状"]}' | go run .
# 缺失内容会被显式拒绝,不当作空消息成功处理。
printf '%s\n' '{"kind":"tags"}' | go run .
接到服务里时我会保留哪些限制
第一,别把 RawMessage 当作绕过解析的通道。整条输入语法不合法时,外层解码也会失败;而类型不匹配要等到相应业务分支才知道。Go 的 Unmarshal 实现会先检查输入是否合法,再做赋值,这解释了两种错误出现位置的差别。Unmarshal 实现
第二,每条消息用新的 Envelope。复用已经有值的目标对象时,缺失字段不一定清除旧内容;这里的局部变量避免把上一条 payload 留到下一条。对本例,缺失 payload 的长度为零,显式 null 保留为 null 片段,两个情况都按本协议拒绝;空对象和空数组则由分支规则决定。
第三,保留输入上限,并在服务入口另设读取超时和请求总量约束。LimitReader 只限制读入多少字节,不负责让一个迟迟不结束的连接及时返回。把命令行例子直接搬到 HTTP 处理器时,需要结合服务本身的请求限制,而不是照抄 os.Stdin。
第四,异步保存时说清谁持有数据。传统 RawMessage 的 UnmarshalJSON 会复制输入片段,但普通 Go 切片赋值仍共享底层存储。若稍后要复用、修改相同 RawMessage,应给异步消费者独立副本,或明确所有权并不再修改。不要把“解码复制过”误读为“之后所有赋值都会深复制”。RawMessage 方法实现
对我来说,代价同样清楚:大字段会占用内存,选择分支后还要解码一次;这不是零拷贝方案,也没有未经测量的性能倍数保证。收益在于模型分层与错误归属更清楚,而不是承诺每条消息一定更快。
怎样判断这套拆法适不适合你
| 输入或需求 | 本例行为 | 应用需要决定的事 |
|---|---|---|
| notice 加对象 | 按 Notice 解释并要求 text 非空 | 是否增加文字长度限制 |
| tags 加字符串数组 | 要求列表和每个标签非空 | 是否去重及限制标签数量 |
| payload 缺失或 null | 明确返回错误 | 自己的协议是否允许空值 |
| 未知 kind | 返回不支持错误 | 拒绝、归档还是另设隔离队列 |
| 一个输入有两个 JSON 值 | 拒绝第二个值 | 是否另建真正的流式协议 |
一定要有 kind 吗?不一定。类型标记也可以来自可信的路由或消息主题;关键是目标类型必须由明确协议决定。本例选择 kind,是为了让一个文件就能说明完整关系。
能保留不认识的内容以后再处理吗?可以设计这样的归档策略,但要同时保存类型、版本和原始内容,并约定重放规则。本例明确拒绝未知 kind,没有偷偷实现归档。
为什么不把 payload 先解成字符串?原本的 JSON 对象或数组不是 JSON 字符串,强行先拿 string 会改变协议。RawMessage 存的是编码片段,不是给对象额外套一层引号。
如果不同业务确实共享信封、内容形状又不同,我会保留这套拆法;如果类型始终固定,直接用一个结构体更直白。到这里,小项目的结果应该是两条正常分支可以独立解释,错误输入会明确失败,接入服务时仍能保留大小、超时与所有权约束。
Go JSON 解析怎样给单个字段设置大小上限
- 上一篇
- Go JSON 解析怎样给单个字段设置大小上限
- 下一篇
- Go Encoder 怎样避免接口输出多余空白
-
- Golang · Go教程 | 12分钟前 |
- Go FlagSet 怎样让子命令独立解析
- 110浏览 收藏
-
- Golang · Go教程 | 13分钟前 |
- Go list 怎样维护 LRU 访问顺序
- 401浏览 收藏
-
- Golang · Go教程 | 15分钟前 |
- Go heap 怎样实现优先级任务队列
- 146浏览 收藏
-
- Golang · Go教程 | 20分钟前 | Go rune unicode strings.IndexFunc 字符串定位
- Go strings.IndexFunc 怎样按字符规则定位位置
- 393浏览 收藏
-
- Golang · Go教程 | 24分钟前 | Go crypto/rand RawURLEncoding Base64url URL令牌
- Go 怎样生成无填充 URL 令牌
- 480浏览 收藏
-
- Golang · Go教程 | 24分钟前 | 并发 · 缓存 · 性能优化 · 正则表达式 · Go教程 · 正则表达式 Go 并发缓存 MustCompile regexp.Compile Regexp缓存
- Go 怎样缓存正则编译结果
- 349浏览 收藏
-
- Golang · Go教程 | 27分钟前 | 文件处理 · 标准库 · Go教程 · io.Copy Go流式读取 Go文件摘要 Go SHA-256 sha256.New
- Go 怎样边读文件边计算摘要
- 133浏览 收藏
-
- Golang · Go教程 | 27分钟前 | Go 单元测试 crypto/rand 依赖注入 io.Reader
- Go crypto/rand 随机源的测试替换方案与生产边界
- 453浏览 收藏
-
- Golang · Go教程 | 30分钟前 |
- Go TLS 怎样处理证书主机名匹配
- 235浏览 收藏
-
- Golang · Go教程 | 33分钟前 | 标准库 · go · https · tls Go crypto/tls crypto/x509 RootCAs 自定义证书
- Go TLS 怎样让客户端信任自定义证书
- 307浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 582次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 620次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 652次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 617次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 434次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

