json.unmarshal不能解析jsonl文件,因换行符非法;应使用bufio.scanner逐行读取并调用json.unmarshal单行解析,避免oom且支持错误容忍与动态schema处理。

JSONL文件不能用json.Unmarshal一次性读取
JSONL(JSON Lines)每行一个合法JSON对象,不是单个JSON数组或对象。直接用json.Unmarshal读整个文件会报错:invalid character '\n' looking for beginning of value——因为换行符不被json包视为合法分隔符。
正确做法是逐行读取、逐行解析。关键不是“怎么快”,而是“怎么稳”:避免内存暴涨、处理不完整行、跳过空行或注释行。
- 用
bufio.Scanner逐行读,比bufio.Reader.ReadLine更安全(自动处理行尾、内置缓冲、不易越界) - 每行
strings.TrimSpace后再判断是否为空或以//开头(有些JSONL变体带注释) - 解析失败时记录行号,不要panic——大数据文件里总有一两行脏数据
定义结构体时注意字段名与JSON key的映射
Go的json标签必须显式匹配JSONL中每行的key名,大小写敏感,且默认忽略未声明字段。如果JSONL字段动态多变(比如日志含不同event_type),硬编码结构体容易panic或丢字段。
两种常见应对方式:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 固定schema:用
type LogEntry struct { Timestamp string `json:"timestamp"` ... },适合ETL预知结构的场景 - 动态schema:用
map[string]interface{}接收,再按需类型断言,例如v, ok := data["duration"].(float64);但要注意嵌套map和float64转int等陷阱 - 别忘了
json.RawMessage:对某些大字段(如原始payload)延迟解析,省GC压力
大文件下避免OOM:用scanner.Scan()流式处理
10GB JSONL文件如果先ioutil.ReadFile或os.ReadFile进内存,直接OOM。必须流式处理,边读边解析边消费(写DB、发Kafka、聚合统计)。
-
scanner := bufio.NewScanner(file)默认缓冲区4KB,对超长行可能截断;若存在超大JSON行,设scanner.Buffer(make([]byte, 64*1024), 1 - 不要在循环里反复
new大结构体;复用变量或对象池(sync.Pool)对高频小对象有效,但JSON解析本身开销不大,优先保证逻辑清晰 - 若需并发解析(如多核CPU),可把文件分块+seek定位起始行(注意UTF-8边界),但实现复杂;多数情况单goroutine + 流式已足够,瓶颈常在IO或下游写入
错误行处理与进度反馈不可省略
真实JSONL文件总有格式错误:少引号、控制字符、BOM头、半截JSON。不处理=程序中途退出=丢失后续全部数据。
- 用
json.Valid(line)快速预检,再进json.Unmarshal,避免冗长错误堆栈 - 记录错误行号和原始内容到单独error log文件,方便事后人工校验
- 每处理N万行打印一次进度(如
log.Printf("processed %d lines", count)),否则跑一小时没输出,你不知道是卡了还是慢 - 注意Windows换行
\r\n和Mac旧换行\r,bufio.Scanner默认兼容,但手动strings.Split会出问题
最易被忽略的是BOM头:某些导出工具会在JSONL首行加\uFEFF,导致第一行解析失败。打开文件后先检查前3字节是否为EF BB BF,是则跳过。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










