当前位置:首页 > 文章列表 > Golang > Go问答 > Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段为什么结果为空或不符合预期

Go问答:json.RawMessage怎样延迟解析不同结构的 JSON 字段为什么结果为空或不符合预期

来源:17golang原创 2026-11-09 19:41:33 0浏览 收藏

当一个 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 不是同一种状态。

JSON 外层字段经过 json.RawMessage 保存后按 kind 分流到具体 Go 结构体的静态说明图
图1:结构说明图展示外层 JSON、RawMessage 和二次解析目标之间的关系,不是运行截图或执行证据。

按 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
目标类型不匹配原始字节仍然存在二次解析返回类型错误,不应忽略

特别要注意:空对象 {} 不是字段缺失。它可能成功解码成一个字段全是零值的结构体;如果业务需要区分“没有数据”和“数据为空”,要在结构体之外保留输入状态,不能只看二次解析后的字段值。

json.RawMessage 对字段缺失、JSON null、合法对象和类型不匹配四种状态的边界说明图
图2:边界说明图对比四类输入的原始字节与二次解析结果,不是运行截图或线上接口返回。

为什么二次解析后字段还是空

如果 RawMessage 有内容,但目标结构体字段仍是零值,优先按下面顺序排查:

  1. 目标字段是否导出。 Go 的 JSON 解码只能写入可设置的导出字段,字段名小写时不会得到输入值。
  2. 标签是否对应实际 JSON。 比如输入是 user_id,目标字段却没有 json:"user_id" 标签,字段映射就可能不符合预期。
  3. kind 与结构是否配套。 把数组交给对象结构体,或者把字符串交给数值字段,二次解析会返回错误;不要只打印目标结构体。
  4. 是否复用了旧变量。 在循环中重复 Unmarshal 到同一个非零结构体时,容易把前一次状态带进下一次判断。每次分支都创建新的目标变量,或显式清空它。
  5. 是否把 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 和目标类型上下文的错误,让调用方区分协议错误与合法零值。

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