登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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) 不会逐字节复制,viewdata 仍指向同一块底层数组。若读取器下一轮把新消息写回 data,保存的 view 就可能被悄悄改写。

Go json.RawMessage 解码时 json.Unmarshal 复制输入与手动切片转换共享底层数组的结构示意图
图1:RawMessage 读取阶段的结构示意;标准库反序列化保存副本,手动切片转换仍可能指向原缓冲区。

把复制放在跨生命周期的边界

复制不应到处散落,而应放在“数据即将离开输入缓冲区”的那个位置。一个足够通用的 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 偷换语义。

Go json.RawMessage 在输入缓冲区、clone、队列和延迟 json.Unmarshal 之间的字节所有权示意图
图2:延迟解析的所有权示意;跨出输入缓冲区生命周期前先复制,异步消费者再独立解析。

延迟解析和并发消费时检查四个节点

第一,解析前不要修改 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 或编码器使用时原地修改,否则仍会产生数据竞争。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>