Go json.RawMessage 延迟解析时如何避免共享底层字节
来源:17golang原创
时间:2026-09-14 20:39:28 369浏览 收藏
用 json.RawMessage 做延迟解析时,最容易误判的是“类型名像字节切片,所以赋值一定安全”。结论很明确:通过 json.Unmarshal 写入 RawMessage 时,标准库会复制输入;但把已有的 []byte 直接转换成 json.RawMessage,或者把一个 RawMessage 直接赋给另一个变量,仍可能共享底层数组。只要原缓冲区还会复用,就应在跨出它的生命周期前复制。
把 RawMessage 留在当前解码对象里通常不需要额外复制;把它交给队列、缓存或另一个 goroutine 时,使用 append([]byte(nil), src...) 建立独立所有权,再做延迟解析。
UnmarshalJSON的契约是把输入保存为副本。json.RawMessage(src)只是切片转换,不是深复制。- 跨缓冲区、队列或 goroutine 传递时,在边界处 clone 一次。
先分清标准库复制和手动共享
RawMessage 本质上是 []byte 的命名类型,价值在于先保留一段合法 JSON,等知道业务类型后再调用 json.Unmarshal。官方实现的 UnmarshalJSON 会用 append 把输入复制到接收者,因此下面这种结构可以直接保存原始字段:
type Envelope struct {
Kind string `json:"kind"`
Payload json.RawMessage `json:"payload"` // 先保存原始 JSON,稍后按 Kind 解析
}
var e Envelope
if err := json.Unmarshal(data, &e); err != nil {
return err // 输入结构不合法时,不把半成品交给后续业务
}
// e.Payload 已经拥有自己的字节副本,可以脱离 data 延迟解析。
危险点在手动提取路径。view := json.RawMessage(data) 不会逐字节复制,view 与 data 仍指向同一块底层数组。若读取器下一轮把新消息写回 data,保存的 view 就可能被悄悄改写。

把复制放在跨生命周期的边界
复制不应到处散落,而应放在“数据即将离开输入缓冲区”的那个位置。一个足够通用的 helper 如下:
func cloneRaw(src []byte) json.RawMessage {
// append 到 nil 切片会分配新底层数组,避免与 src 共享存储
dst := append([]byte(nil), src...)
return json.RawMessage(dst)
}
func retainForQueue(frame []byte) json.RawMessage {
// 入队后由其他 goroutine 消费,所以这里必须先取得所有权
return cloneRaw(frame)
}
如果项目最低版本允许,也可以用 bytes.Clone 表达同样的意图。关键不是 helper 的名字,而是让调用者一眼看出返回值不再依赖输入缓冲区。对空切片要保留语义:append([]byte(nil), nil...) 得到 nil,后续 Marshal 时会按 RawMessage 的规则表现为 JSON null;如果业务需要空数组,应明确传入 [],不要用 clone 偷换语义。

延迟解析和并发消费时检查四个节点
第一,解析前不要修改 RawMessage 的内容。它可以作为输入交给 json.Unmarshal,但不要一边交给消费者一边对同一底层数组做原地清洗。第二,切片赋值只是复制三元组(指针、长度、容量),例如 saved := current 不会产生独立数据;要独立保存就调用 cloneRaw(saved)。
第三,交给 goroutine 的不是“变量”,而是它引用的字节。即使外层结构体没有再变化,只要来源 buffer 会被复用,仍然可能发生数据竞争或内容漂移。第四,重新编码时 RawMessage.MarshalJSON 会直接返回自身字节,因此它也不替你创建并发安全的快照;需要快照的场景应在进入共享区域前完成复制。
| 来源 | 能否直接长期保存 | 建议 |
|---|---|---|
json.Unmarshal 写入 RawMessage | 可以 | 标准库已复制输入 |
json.RawMessage(src) | 通常不可以 | 先 clone,再入队或缓存 |
| RawMessage 直接赋值 | 不可以 | 按需要复制底层字节 |
| 同一 goroutine 内立即解析 | 视来源而定 | 不跨生命周期时可避免多余复制 |
用一张清单固定项目里的判断
可以把下面四个问题放进 code review:这段字节来自标准库解码还是复用 buffer?RawMessage 会不会进入队列、缓存或 goroutine?持有期间是否有人会原地写入源切片?下游是否需要一个不可变快照?只要前三问出现“会”,或者最后一问是“需要”,就在边界处复制一次。
实践中不要为了“保险”在每次 json.Unmarshal 前后都复制。那会制造额外分配,却没有增加安全性。更稳妥的路线是:标准库解码结果直接使用;手动切片转换先 clone;跨异步或缓存边界时明确转移所有权;完成快照后不再修改其字节。
相关问题
RawMessage 延迟解析是不是一定比直接解析更快?
不一定。它适合先根据类型字段选择目标结构,或只处理部分消息;如果最终所有字段都会解析,延迟本身不会消除解析成本。
RawMessage 和 []byte 直接比较有什么问题?
两者都是字节序列,不能用 == 比较;需要内容比较时使用 bytes.Equal,并先确认双方是否代表同一种 JSON 形式。
复制一次后还能修改 RawMessage 吗?
可以,但应把它视为当前持有者的私有数据。不要在它已经交给其他 goroutine 或编码器使用时原地修改,否则仍会产生数据竞争。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
327 收藏
-
Golang · Go教程 | 45分钟前 | 文件读取 · Go教程 · CSV解析 · encoding/csv · csv.Reader · Go csv.Reader Comment Go CSV 跳过注释行 encoding/csv 注释字符 csv.Reader 前导空白215 收藏
-
454 收藏
-
372 收藏
-
217 收藏
-
147 收藏
-
237 收藏
-
204 收藏
-
115 收藏
-
440 收藏
-
276 收藏
-
Golang · Go教程 | 3小时前 | Context · 并发控制 · Go教程 · 请求生命周期 · Go Deadline context.Context 取消信号 context.WithoutCancel374 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习