并发读大文件不能用 os.readfile,应手动分块并用 io.readat 配合独立文件句柄;需校验文件系统随机读支持、复用缓冲区、加 panic 恢复与 context 控制。

直接用 os.ReadFile 并发读大文件会 OOM
这不是并发没写对,而是根本用错了 API。每个 goroutine 调用 os.ReadFile 都会把整份文件(比如 10GB)全 load 进内存,哪怕只取其中一行——结果是瞬间触发 GC 压力、runtime 内存耗尽,甚至被系统 kill。真正要并发读,必须手动切块、各协程只读自己那一段。
- 先用
os.Stat().Size()拿到总字节数,再按固定块大小(如 4MB)算出每段的offset和length - 最后一块长度 = 总大小 - 最后一个 offset,不能硬写
4 * 1024 * 1024,否则越界返回io.EOF或数据错乱 - 每个 goroutine 必须调用
os.Open单独打开文件,拿到独立的*os.File句柄;共享一个句柄并发调io.ReadAt在某些文件系统上会出隐含状态冲突
io.ReadAt 是分块读的唯一可靠选择
bufio.Scanner 看起来方便,但它内部缓冲不可控、不支持指定起始位置,且默认按行切割——对二进制或无换行的大文件完全失效。而 io.ReadAt 是纯偏移+长度语义,不依赖当前文件 offset,也不触发 Seek 系统调用,零拷贝、无额外分配,天然适配分块场景。
- 确保文件支持随机读:本地 ext4/xfs 没问题,但 NFS、CIFS 或只读挂载可能不支持,上线前得实测
- 缓冲区别在 goroutine 里反复
make([]byte, blockSize),用sync.Pool复用更稳,尤其块数 > 100 时效果明显 - 缓冲区大小建议设为 64KB 或 1MB——太小导致 syscall 过多,太大浪费内存且增加 GC 扫描压力
worker 池必须带取消和 panic 恢复
一个没 recover 的 panic 或卡死的 I/O(比如某块读取 hang 住),会让整个 worker 退出,池子悄悄“缩水”,后续任务堆积没人处理。这不是理论风险,是线上高频事故。
- worker 循环必须包在
defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r) } }()里 - 不能写
for path := range ch这种裸循环,得用select+context.Context支持主动关闭:比如超时、服务 reload 时优雅退出 - 任务 channel 建议用小缓冲(如
make(chan Task, 16)),避免积压掩盖真实瓶颈;非阻塞提交时用select { case ch
合并结果时必须按 offset 排序
goroutine 执行顺序不确定,先启动的不一定先完成。如果直接 append 到 slice,最终数据顺序就乱了——对需要严格顺序的场景(如日志解析、二进制协议还原),这是致命错误。
- 每个任务执行完,把结果连同原始
offset一起发回汇总 channel - 主 goroutine 收齐所有块后,按
offset字段排序,再拼接字节 slice - 别用 map 存结果——map 遍历无序,且 key 是 int 时容易误以为有序
分块边界对齐、句柄隔离、panic 恢复、结果排序——这四点漏掉任何一环,都可能让“高并发读大文件”变成线上事故现场。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











