当前位置:首页 > 文章列表 > Golang > Go教程 > Go JSON 怎样延迟解析不同类型字段

Go JSON 怎样延迟解析不同类型字段

来源:17golang原创 2026-10-21 23:22:33 0浏览 收藏

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 文档

Envelope由Kind和Payload组成,Payload保留JSON片段并与具体业务类型分开
图1:信封数据结构说明图。看“外层协议”和“内容表示”两个分组;连线表示字段组成与表示关系,不表示执行次序,也不是运行截图。

准备一个只依赖标准库的小工具

准备可正常使用标准库的 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 字段规则

describe按kind关联Notice和字符串数组,Notice对应Text,标签数组对应非空标签规则
图2:业务类型关系说明图。左侧分组放分派约定,右侧分组放具体内容及其规则;线只表达类型关联,不表示时间顺序或实际运行结果。

用两种输入看懂程序该做什么

先试通知对象,再试标签数组。下面是可复制的命令;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 存的是编码片段,不是给对象额外套一层引号。

如果不同业务确实共享信封、内容形状又不同,我会保留这套拆法;如果类型始终固定,直接用一个结构体更直白。到这里,小项目的结果应该是两条正常分支可以独立解释,错误输入会明确失败,接入服务时仍能保留大小、超时与所有权约束。

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