os.ReadFile读大文件会卡住是因为它一次性加载全部内容到内存,易触发OOM;应改用bufio.Scanner配合适当缓冲区,或io.ReadAt实现并发安全读取。

为什么os.ReadFile读大文件会卡住
因为os.ReadFile会一次性把整个文件加载进内存,文件大小超过几百MB时,不仅GC压力陡增,还可能触发OOM。这不是“慢”,是设计上就不该用它读大文件。
- 它底层调用
os.Open+io.ReadAll,没有流式控制 - 返回
[]byte,无法复用缓冲区,每次调用都分配新内存 - 对GB级日志、CSV、二进制dump等场景完全不适用
用bufio.Scanner逐行读但遇到长行就panic
bufio.Scanner默认最大行长度是64KB,超长行(比如单行JSON、base64编码块)直接报scanner: token too long。不能只改Split函数,得重设缓冲区。
sc := bufio.NewScanner(f)
buf := make([]byte, 10*1024*1024) // 10MB缓冲
sc.Buffer(buf, 10*1024*1024)
sc.Split(bufio.ScanLines)
for sc.Scan() {
line := sc.Bytes() // 注意:line是buf子切片,别逃逸出循环
}
- 必须同时调用
sc.Buffer()和sc.Split(),顺序不能反 -
sc.Text()会拷贝字符串,高吞吐下建议用sc.Bytes()避免重复分配 - 缓冲区过大可能浪费内存,按实际最长行预估,别无脑设100MB
需要随机读取或跳过头部,io.ReadAt比Seek+Read更稳
在多goroutine并发读同一文件时,file.Seek()操作不是线程安全的——它修改文件内部偏移量,多个goroutine互相覆盖。而io.ReadAt传入明确偏移,不依赖文件状态。
- 用
file.ReadAt(p []byte, off int64)替代file.Seek()+file.Read() - 注意
off必须在文件范围内,否则返回io.EOF或io.ErrUnexpectedEOF - 对只读文件句柄,
ReadAt可安全并发;但写入时仍需同步
内存映射mmap适合只读大文件但Windows支持弱
Go标准库没内置mmap,得靠golang.org/x/sys/unix(Linux/macOS)或golang.org/x/sys/windows(Windows)。Windows的CreateFileMapping行为和POSIX差异大,尤其对稀疏文件或网络驱动器容易失败。
- Linux下用
unix.Mmap+unix.Munmap,性能接近零拷贝 - Windows需额外处理
SEC_COMMIT标志和页面对齐,错误码更难调试 - 小文件(mmap反而更慢,因系统调用开销和页表建立成本
真正卡点往往不在读取本身,而在后续解析——比如把每行JSON反序列化成map[string]interface{},这比IO慢十倍。先确认瓶颈在IO还是CPU,别一上来就换mmap。











