不能直接用 json.Unmarshal 解大文件,因为它需将整个 JSON 一次性加载进内存,易引发 OOM 或 GC 压力;而 json.Decoder 基于 io.Reader 流式解析,内存占用稳定。

为什么不能直接用 json.Unmarshal 解大文件
因为 json.Unmarshal 要求整个 JSON 数据一次性加载进内存,一个 500MB 的 JSON 文件会直接申请近似大小的内存,还可能触发 GC 压力或 OOM。而 json.Decoder 基于 io.Reader,边读边解析,内存占用稳定在几 KB 到几 MB,取决于嵌套深度和字段长度。
如何用 json.Decoder 逐个解析 JSON 数组元素
常见大 JSON 是数组格式(如日志、导出数据),但 json.Decoder 默认只解一个顶层值。必须手动跳过左括号、循环读取每个元素、处理逗号分隔、识别右括号结束——否则会卡在第一个对象后报 invalid character ',' looking for beginning of value。
- 先调用
decoder.Token()读取[,确认是数组起始 - 用
for decoder.More() { ... }循环,它内部自动处理逗号和边界 - 每次循环内用
decoder.Decode(&item)解单个对象(item是结构体或map[string]interface{}) - 遇到非法 JSON(如某行字段缺失)时,
Decode返回 error,可记录位置并continue,不影响后续
<pre class="brush:php;toolbar:false;">file, _ := os.Open("data.json")
defer file.Close()
decoder := json.NewDecoder(file)
decoder.UseNumber() // 避免 float64 精度丢失,后续可用 Number.Int64() 等
// 跳过 '['
if t, _ := decoder.Token(); t != json.Delim('[') {
log.Fatal("expected [")
}
for decoder.More() {
var item MyRecord
if err := decoder.Decode(&item); err != nil {
log.Printf("parse error at offset %d: %v", decoder.InputOffset(), err)
continue // 跳过坏数据,继续下一条
}
process(item)
}
解析嵌套对象或混合结构时 Token() 手动遍历的必要性
如果 JSON 不是扁平数组(比如顶层是对象,内含一个大数组字段 "items": [...]),就必须用 <code>Token() 逐层下降:读键名 → 检查是否为 "items" → 读 [ → 进入 More() 循环。跳过无关字段能显著提速,尤其当大数组只是顶层的一个字段时。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
decoder.Token()获取当前 token,返回string、json.Delim、float64等类型 -
json.Delim('{')和json.Delim('[')可用于判断结构层级 - 对不需要的字段,调用
decoder.Skip()直接跳过整棵子树(比 Decode 到空 struct 更快) - 注意:一旦调用
Skip(),该 token 后续不能再用Decode,状态已推进
性能与兼容性关键点
json.Decoder 默认不校验 UTF-8,若源 JSON 含非法编码会静默截断;另外,UseNumber() 开启后所有数字都存为 json.Number 字符串,避免浮点精度问题,但需手动转 int64 或 float64,否则序列化回 JSON 会多一层引号。
- 大文件建议搭配
bufio.NewReaderSize(file, 64*1024),减少系统调用,提升吞吐 - 并发解析不可直接复用同一个
decoder,但可为每个 goroutine 创建新decoder并共享底层io.Reader(需加锁或使用io.MultiReader分片) - Go 1.20+ 中
decoder.DisallowUnknownFields()可捕获未知字段,但会中断整个流,慎用 - 错误恢复能力弱:一旦底层 reader 返回 EOF 或 I/O error,decoder 无法重置,只能重建
最易被忽略的是 InputOffset() —— 它返回解析器当前字节偏移,不是文件指针位置,在跳过非法段或调试错位时是唯一可靠的定位依据。

















