Golang如何高效读取大文件内容

bufio.Scanner默认64KB缓冲区遇超长行报错,需调用scanner.Buffer扩容;大文件禁用ReadFile,应依场景选Scanner、Reader或流式解析器;Reader.Read须检查io.EOF;mmap跨平台差且未必更快;顺序读勿滥用O_DIRECT。

bufio.Scanner 逐行读取时内存不爆但容易丢数据

默认情况下 bufio.Scanner 的缓冲区只有 64KB,遇到超长行(比如单行 JSON、日志中带大段 base64)会直接报错 scanner: token too long。这不是性能问题,是安全限制,但很多人误以为是“读得慢”。

解决方法是手动扩容缓冲区:

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024) // 初始 64KB,动态增长
scanner.Buffer(buf, 10*1024*1024) // 最大允许 10MB 行长

注意第二参数不能设为 math.MaxInt32 —— 某些系统调用会因过大的值返回 EINVAL

真正的大文件(GB 级)别用 ioutil.ReadFileos.ReadFile

这两个函数会把整个文件一次性加载进内存,哪怕文件只有 500MB,也极可能触发 OOM 或让 GC 压力陡增。Golang 进程 RSS 突然飙高、卡顿几秒,往往就是这个原因。

立即学习“go语言免费学习笔记(深入)”;

替代方案取决于你要做什么:

  • 只统计行数或简单匹配?用 bufio.Scanner + 自定义分隔符
  • 需要随机访问某几行?先用 bufio.Reader 配合 Seek 定位,再读小块
  • 要解析 CSV/JSONL?用流式解析器(如 encoding/csvcsv.NewReader,或 jsonl 包)

bufio.Reader.Read 手动控制读取粒度更灵活但易出错

当 Scanner 不够用(比如按固定字节块处理、跳过 BOM、处理粘包式二进制格式),就得退到 bufio.Reader。它的 Read 方法返回实际读到的字节数,必须检查 err == io.EOF 而非仅靠返回长度判断结束。

常见错误写法:

for {
    n, err := reader.Read(buf)
    if n == 0 { break } // ❌ 错!n==0 不代表结束,可能是临时阻塞或空行
    // ...
}

正确写法:

for {
    n, err := reader.Read(buf)
    if n > 0 {
        // 处理 buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        // 处理其他错误(如 io.ErrUnexpectedEOF)
        break
    }
}

Linux 下用 mmap 不一定更快,且跨平台差

有人会想到用 golang.org/x/sys/unix.Mmap 做内存映射。它在某些场景(如反复随机读同一块大文件)确实减少拷贝,但代价明显:

  • Windows 不支持,syscall.Mmap 在 Windows 上行为不同
  • 映射后仍需手动管理 Munmap,漏掉会导致资源泄漏
  • 对 SSD 友好,但对 NFS 或 FUSE 文件系统可能反而变慢
  • Go 的 GC 不感知 mmap 内存,可能导致 RSS 报告失真

除非你明确压测对比过,并确认瓶颈真在内核拷贝而非业务逻辑,否则优先用 bufio + 合理 buffer size。

最常被忽略的一点:文件打开时的 flag。如果只是顺序读,加上 os.O_RDONLY | syscall.O_DIRECT(Linux)或 syscall.FILE_FLAG_NO_BUFFERING(Windows)看似能绕过 page cache,但实际会显著降低吞吐——因为失去了预读和合并 IO 的优势。普通场景老老实实用默认打开方式就行。