批处理易oom因默认全量加载,需分批并控内存;bufio.scanner默认64kb缓冲易爆错,应调大或改用reader;切片需预分配与复用减gc;并发批处理须用信号量限流,不可仅靠waitgroup。

批处理不控内存,跑着跑着就 OOM 了。这不是配置问题,是设计惯性导致的——默认把整批数据塞进内存再处理,而真实场景里,数据源(文件、DB、HTTP)往往远超可用内存。关键不是“要不要分批”,而是“怎么让每一批不拖垮系统”。
bufio.Scanner 读大文件时爆 scanner: token too long
这是最常被忽略的缓冲区陷阱。默认 bufio.Scanner 只给单行预留 64KB 缓存,遇到带长 JSON 字段的日志、宽表 CSV 或拼接 SQL,直接报错中断。
- 必须显式调大缓冲区:
scanner.Buffer(make([]byte, 1024*1024), 1024*1024*4)—— 初始 1MB,上限 4MB - 若行宽不可控(比如混入 base64 大字段),改用
bufio.Reader.ReadLine()+ 手动拼接更稳妥 - 增大缓存 ≠ 解决根本问题:如果某行真有 100MB,得靠前置格式校验或切分逻辑拦截,不能只依赖 Buffer
切片预分配与复用:避免高频 GC 压力
批量处理中反复 make([]T, 0) 创建切片,会触发大量小对象分配,GC 频繁扫描堆,CPU 时间全耗在回收上。
- 预分配容量:处理已知规模数据时,用
make([]Item, 0, expectedCount)直接设够容量 - 复用底层数组:传入已有切片做“接收器”,用
result = result[:0]清空长度而非新建,减少分配次数 - 注意:复用只适用于生命周期可控的局部切片;跨 goroutine 或长期持有需谨慎,避免数据污染
并发批处理必须配信号量,别只靠 sync.WaitGroup
sync.WaitGroup 只管“等完”,不管“同时跑几个”。直接对每个 batch 起 goroutine,几千个协程瞬间压垮内存和下游服务。
- 用
chan struct{}实现轻量信号量:sem := make(chan struct{}, 10)控制最大并发 10 - 每个任务前
sem ,结束后 <code>(建议 defer) - 搭配
errgroup.Group统一收口错误和等待,比裸写WaitGroup+ 全局 error 更可靠 - 切记:信号量 channel 不能 close,要复用;goroutine 内不要 recover 后吞错误,至少 log 并计数
流式处理不是“选配”,是内存安全的底线
从文件、数据库、HTTP 流读取数据时,“加载全部再处理”是危险路径。流式本质是控制数据流动节奏,让内存占用恒定可预期。
- 文件:用
bufio.Scanner或bufio.Reader按行/按块读,处理完立即丢弃 - 数据库:用
rows.Next()分页游标,避免rows.Slice()一次性取全量 - HTTP 请求体:传
io.Reader(如strings.NewReader、os.File)给http.NewRequest,不缓存全文本 - 核心判断标准:任意时刻内存中驻留的数据量,不应随输入总大小线性增长
真正难的不是写出分批逻辑,而是让每一批的内存边界清晰、可测量、不漂移。上线前用 pprof 抓一次堆快照,看峰值是否落在预期区间内——没这步,优化就是纸上谈兵。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











