json.Marshal 不适合大文件流式写入,因其需将全部数据加载进内存,导致内存占用激增、GC压力大且无法边生成边写;应改用 json.Encoder 实现真正的流式编码。

json.Marshal 为什么不适合大文件流式写入
直接用 json.Marshal 把整个结构体转成 []byte 再写入文件,会一次性把全部数据加载进内存。对几百 KB 的数据还行,但一旦结构体含大量 slice、嵌套 map 或字段超过 20 个,内存占用常翻 2–3 倍,GC 压力陡增;更关键的是,你无法边生成边写——所有数据必须构造完毕才能开始 IO。
- 常见错误现象:
runtime: out of memory或 GC pause 明显变长(尤其在 HTTP handler 中) - 性能影响:反射开销 + 内存拷贝 + 临时分配,吞吐量随数据规模非线性下降
- 兼容性注意:若目标是
io.Writer(如os.File、http.ResponseWriter),json.Marshal返回的是[]byte,需额外调用Write,多一次拷贝
用 json.Encoder 替代 Marshal 实现真正流式写入
json.Encoder 是标准库里专为流式场景设计的接口,它把序列化和写入合并成一步,不缓存完整 JSON,而是边编码边写到底层 io.Writer。
- 正确用法:
enc := json.NewEncoder(w); enc.Encode(v)——v可以是 struct、slice、map,甚至单个值 - 关键优势:零中间
[]byte分配,内存占用恒定(约几 KB),天然支持网络流和大文件 - 容易踩的坑:如果
w是http.ResponseWriter,必须确保没提前写 header(否则Encode会 panic);需要 flush 时,确认w实现了http.Flusher - 示例:写入文件时,直接
json.NewEncoder(f).Encode(users),比f.Write(json.Marshal(users))更快更省
处理动态数据流(如 chan)时手动控制 JSON 结构
json.Encoder 本身不识别 chan 类型,直接传会报 json: unsupported type: chan string。此时不能依赖自动编码,得手动拼 JSON 框架 + 分块编码元素。
- 典型场景:从数据库游标或消息队列持续读取记录,逐条转 JSON 写入文件或响应流
- 实操步骤:先写
{或[,再循环enc.Encode(item),每次后加逗号(除最后一项),最后写}或] - 注意点:必须自己管理分隔符和括号匹配;推荐用
bufio.Writer包一层提升小数据写入效率 - 别踩的坑:别用
fmt.Fprintf(w, "%s", string(jsonBytes))—— 这本质还是全量 marshal,只是换了个写法
替换字段值或修改结构时优先选 token 级流式扫描器
当需求不是“序列化 Go 数据”,而是“读一个 30MB JSON 文件,把所有 "title" 字段值替换成新字符串”,json.Unmarshal + struct 完全不合适:结构未知、内存爆炸、还得定义几十层嵌套类型。
立即学习“go语言免费学习笔记(深入)”;
- 推荐工具:
github.com/benbjohnson/megajson/scanner—— 轻量级词元扫描器,只认TSTRING、TCOLON等基础 token - 核心逻辑:监听连续出现的
"title"(TSTRING)→TCOLON→ 下一个TSTRING,定位到值位置后原地替换 - 为什么不用
json.Decoder?它仍需构建 interface{} 树,内存不恒定;而 scanner 只维护几个状态变量,30MB 文件内存占用稳定在 ~1KB - 限制:不处理语法错误恢复,输入必须是合法 JSON;替换操作需自己处理字符串编码边界(如引号、转义)
Marshal 构造中间字节切片;或者以为用了 Decoder 就够轻量,却没意识到它仍会把整个子对象解码成 map[string]interface{}。


















