当前位置:首页 > 文章列表 > Golang > Go问答 > Go json.RawMessage 为什么需要复制后再长期保存

Go json.RawMessage 为什么需要复制后再长期保存

来源:17golang原创 2026-09-27 20:30:12 0浏览 收藏

把一段 JSON 先保留为 json.RawMessage,等知道事件类型后再解析,是 Go 中很实用的延迟解码方式。真正容易出问题的地方不是 JSON 语法,而是这段字节由谁拥有、能保存多久。结论是:通过 json.Unmarshal 解码到 RawMessage 时,标准库已经复制了输入;如果直接把一个可能复用的 []byte 转成 RawMessage,长期保存前就必须建立自己的副本。

官方文档:https://pkg.go.dev/encoding/json

先记住三点
  • RawMessage 本质是 []byte,类型转换不会自动复制底层数组。
  • UnmarshalJSON 的约定是把输入数据复制到消息自身,所以不要无条件二次复制。
  • 来自网络帧、对象池或临时读取区的字节,如果要跨越缓冲区复用周期保存,先用 bytes.Clone 建立独立所有权。

RawMessage 到底复制了什么

json.RawMessage 是一个字节切片类型,同时实现了 Marshaler 和 Unmarshaler。当它作为结构体字段参与 json.Unmarshal 时,标准库会调用它的 UnmarshalJSON 方法;该方法把传入的 data 复制到接收者中。这里的复制让解码后的消息不再依赖解码器当时使用的输入区。

json.Unmarshal 与 RawMessage 复制边界的静态结构说明图
图1:RawMessage 字节所有权说明图,区分标准库解码时的复制边界与延迟解析对象。
type Envelope struct {
    Kind string          `json:"kind"`
    Body json.RawMessage `json:"body"` // 先保留原始 JSON,稍后按 Kind 决定目标类型
}

func decodeEnvelope(input []byte) (Envelope, error) {
    var env Envelope
    // UnmarshalJSON 会把 Body 的 data 复制到 RawMessage 中,返回后可独立保存。
    if err := json.Unmarshal(input, &env); err != nil {
        return Envelope{}, err // 输入不是合法 JSON 时保留错误,不缓存半成品
    }
    return env, nil
}

相反,下面的写法只是把切片换了一个名字。raw 与 input 可能指向同一块底层数组;如果调用方随后复用 input,长期缓存里的内容也会被改写。

func viewAsRaw(input []byte) json.RawMessage {
    // 类型转换只改变切片的静态类型,不会创建新的底层数组。
    return json.RawMessage(input)
}

长期保存时如何建立自己的字节副本

消息来自网络读取缓冲区、对象池或批量解码器时,通常不能假设输入切片会一直不变。长期保存的边界可以是事件队列、内存缓存、异步 goroutine,甚至只是函数返回后仍要使用的对象。此时把复制动作放在“取得所有权”的位置最清楚。

可复用输入缓冲区与 RawMessage 独立副本的静态结构说明图
图2:长期保存原始 JSON 的结构说明图,查看可复用缓冲区与独立副本的边界。
import (
    "bytes"
    "encoding/json"
)

func retainRaw(input []byte) json.RawMessage {
    // Clone 让返回值拥有独立底层数组,可跨过 input 的复用周期保存。
    owned := bytes.Clone(input)
    return json.RawMessage(owned)
}

func decodeBody(raw json.RawMessage, target any) error {
    // 延迟解析只读取 RawMessage,不修改原始字节;错误由调用方决定是否丢弃事件。
    return json.Unmarshal(raw, target)
}

如果项目仍需兼容没有 bytes.Clone 的旧工具链,也可以使用 append([]byte(nil), input...)。不要写成 append(input[:0], input...) 后再把结果当成无条件安全:当容量足够时,它仍可能复用输入的底层数组。

复制前先看来源、容量和生命周期

“复制后保存”不是要求每次拿到 RawMessage 都再复制一次。判断时看三个问题:它是否来自标准库的 Unmarshal;来源切片是否会被写入或复用;保存对象的生命周期是否超过来源缓冲区。只有后两项存在风险时,才需要在边界处复制。

取得方式是否默认独立长期保存建议
json.Unmarshal(input, &raw)是,RawMessage 已复制直接保存,避免无意义二次分配
json.RawMessage(input)否,可能共享数组输入会复用时使用 bytes.Clone
raw[:n] 或从大缓冲区截取否,仍受原数组影响确认所有权后再复制
json.Marshal(value) 返回的字节由返回切片持有交给异步任务前不要让其他代码改写它

还要留意大数组的“隐性持有”:只截取一小段并不能释放原来的大容量,长期对象可能把整块缓冲区留在内存中。需要隔离容量和生命周期时,复制小片段往往同时解决数据竞争风险与内存滞留问题。

延迟解析的最小完整写法

下面的例子把事件的公共字段和可变类型的主体分开。解码入口使用标准库复制语义,后续只在确定类型后解析一次;如果事件来自外部复用缓冲区,则把 retainRaw 放在进入队列前。

type LoginBody struct {
    UserID string `json:"user_id"`
}

type LogoutBody struct {
    Reason string `json:"reason"`
}

func parseEvent(input []byte) (string, any, error) {
    var env Envelope
    // 先解析公共字段和 RawMessage;Body 在这里已经拥有自己的字节副本。
    if err := json.Unmarshal(input, &env); err != nil {
        return "", nil, err
    }

    var target any
    switch env.Kind {
    case "login":
        target = new(LoginBody)
    case "logout":
        target = new(LogoutBody)
    default:
        return env.Kind, nil, fmt.Errorf("unsupported event kind: %s", env.Kind) // 未知类型不猜结构
    }
    // 按已确认的类型解析;畸形主体直接返回错误,不把半解析对象放入缓存。
    if err := json.Unmarshal(env.Body, target); err != nil {
        return env.Kind, nil, err
    }
    return env.Kind, target, nil
}

审查这类代码时,优先追踪切片来源,而不是看到 RawMessage 就机械添加复制。标准库解码、直接类型转换和大缓冲区截取是三条不同路径。

四个容易混淆的边界

  • “RawMessage 是值类型”不等于“字节自动深拷贝”。切片值包含指针、长度和容量,复制切片头仍可能共享数组。
  • “Unmarshal 会复制”不等于“所有入口都会复制”。它只描述实现了 UnmarshalJSON 的解码路径。
  • “当前没有并发”不等于“可以长期引用输入”。对象池归还、下一帧读取和复用写入都可能在异步代码之外发生。
  • “复制越多越安全”也不成立。不必要的二次复制会增加分配;应在所有权交接处做一次有目的的复制。

结语:把复制放在所有权交接处

Go json.RawMessage 是否需要复制,关键不在类型名称,而在字节的来源和生命周期。标准库从 JSON 解码到 RawMessage 时已经完成一次安全复制;手动把外部 []byte 转为 RawMessage 时,则要在进入长期对象、队列或缓存前用 bytes.Clone 建立独立副本。沿着这条边界设计,既能保住延迟解析的灵活性,也能避免共享缓冲区导致的内容漂移和大数组滞留。

相关问答

json.RawMessage 需要每次 json.Unmarshal 后再 bytes.Clone 吗?

通常不需要。解码到 RawMessage 的标准路径已经复制输入;只有当你无法确认它来自该路径,或后续又把外部切片直接赋给它时,才在所有权交接处复制。

用 append 复制 RawMessage 时最重要的注意点是什么?

目标必须是新的或明确独立的切片,例如 append([]byte(nil), src...);不要依赖对已有切片的原地 append,因为它可能复用原底层数组。

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