Go json.RawMessage 为什么需要复制后再长期保存
来源:17golang原创
时间:2026-09-27 20:30:12 330浏览 收藏
把一段 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 复制到接收者中。这里的复制让解码后的消息不再依赖解码器当时使用的输入区。

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,甚至只是函数返回后仍要使用的对象。此时把复制动作放在“取得所有权”的位置最清楚。

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,因为它可能复用原底层数组。
-
261 收藏
-
357 收藏
-
225 收藏
-
301 收藏
-
389 收藏
-
225 收藏
-
240 收藏
-
497 收藏
-
233 收藏
-
171 收藏
-
269 收藏
-
248 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习