两个杂技兄弟:Unmarshal 和 Decode
如果您使用 Go 编程并处理 JSON,那么迟早会面临选择:使用 json.Unmarshal 还是 json.NewDecoder.Decode?这个问题非常流行,以至于在 Stack Overflow 上它获得了数千次浏览和数十次投票。但您在那里找到的答案通常归结为“取决于任务”。
今天,我们不仅仅给出一个抽象的回答。我们将进行自己的调查:剖析这两种方法的内部机制,编写基准测试,展示真实的代码示例,并得出何时使用哪种方法的明确规则。系好安全带——会很精彩!
简要说明:什么是 Unmarshal 和 Decode?
这两个工具做的是同一件事——将 JSON 转换为 Go 结构体。但它们的做法不同。
json.Unmarshal— 接收[]byte和一个指向结构体的指针。一步到位:字节有了,结构体准备好了,开始吧。它“直接”工作——在内存中反序列化整个数据数组。json.NewDecoder.Decode— 处理流(io.Reader)。您创建一个解码器,向其传递一个读取器(例如,os.File或http.Response.Body),然后调用Decode。解码器分块读取数据,不会立即将所有内容加载到内存中。
看起来区别很明显:对于大数据使用 Decoder,对于小数据使用 Unmarshal。但魔鬼总是在细节中。
内部机制:它们在底层是如何工作的
json.Unmarshal — 精准射击
Unmarshal 是一个函数,它:
- 接收一个字节切片。
- 为结果结构体分配内存。
- 一次性解析整个 JSON 并填充结构体。
一切都简单而优雅。但有一个细微之处:如果您向 Unmarshal 传递一个大的 JSON(例如,100 MB),那么您首先必须将这 100 MB 读入内存。然后 Unmarshal 会为结构体再分配内存。结果是 RAM 消耗翻倍。
json.NewDecoder.Decode — 水管工
Decoder 的工作方式不同:
- 创建一个新的解码器,它包装了
io.Reader。 - 解码器以缓冲区(默认 4 KB)为单位读取数据。
- 在读取过程中,它逐个令牌地解析 JSON 并填充结构体。
优点——您不必将整个 JSON 存储在内存中。缺点——管理缓冲区和流式读取的开销稍大。
实验:基准测试不会说谎
让我们在实践中验证理论。为三种场景编写一个简单的基准测试:
- 小 JSON(1 KB)
- 中等 JSON(100 KB)
- 大 JSON(10 MB)
package main
import ( "bytes" "encoding/json" "testing")
type Data struct { ID int `json:"id"` Name string `json:"name"` Items []int `json:"items"`}
func generateJSON(size int) []byte { data := Data{ ID: 1, Name: "test", Items: make([]int, size), } for i := 0; i < size; i++ { data.Items[i] = i } b, _ := json.Marshal(data) return b}
func BenchmarkUnmarshal(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { json.Unmarshal(jsonData, &result) }}
func BenchmarkDecoder(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { reader := bytes.NewReader(jsonData) json.NewDecoder(reader).Decode(&result) }}结果(在我的笔记本电脑上,Go 1.22):
大小
博客书籍推荐推荐阅读 |
|---|