该用mmap而非bufio.Read的场景是频繁随机访问大文件或文件远超内存时;因mmap支持按需分页加载与任意偏移直接访问,而bufio.Reader仅顺序缓冲读取,无法高效跳转。

什么时候该用 mmap 而不是 bufio.Read
当你需要频繁随机访问大文件(比如索引查找、跳转读取某段二进制数据),或文件大小远超可用内存(几十 GB 以上),bufio.Reader 就会暴露短板:它本质仍是顺序预读 + 缓冲,跳到文件末尾附近得先读完中间所有内容;而 mmap 让你像访问内存切片一样直接取任意偏移,操作系统按需加载页,不触发冗余拷贝。
典型适用场景包括:
- 只读型数据库快照(如 bbolt 底层)
- 日志文件中按时间戳范围快速定位起始位置
- 图像/音频元数据提取,需反复跳转解析 header 和 chunk
不适合的场景:小文件(msync)、容器环境内存受限、Windows 下部分第三方库支持弱。
用 github.com/edsrzf/mmap-go 实现安全映射
Go 标准库不提供跨平台 mmap 支持,golang.org/x/exp/mmap 仅限 read 且已归档,目前最稳定的是 github.com/edsrzf/mmap-go。它封装了 syscall 层差异,支持 Linux/macOS/Windows。
关键操作步骤:
- 用
mmap.Open打开只读文件,返回*mmap.MMap - 调用
mm.Bytes()获取可直接切片访问的[]byte - 读取任意区间如
data[1024:2048],无需Seek或ReadAt - 务必调用
mm.Unmap()释放映射(defer 最安全)
示例:
mm, err := mmap.Open("data.bin")
if err != nil {
log.Fatal(err)
}
defer mm.Unmap()
data := mm.Bytes()
header := data[:512] // 直接取前512字节
chunk := data[10*1024*1024 : 10*1024*1024+4096] // 跳到第10MB读4KB
mmap 的真实开销和容易踩的坑
mmap 不是银弹,几个关键点常被忽略:
- 映射本身不加载全部内容,但首次访问某页会触发缺页中断——若你顺序扫完整个映射,性能未必比
bufio好,甚至更差(因少了预读优化) - 映射区域计入进程虚拟内存,
top看 RSS 可能不高,但 VIRT 会暴涨;在内存紧张机器上可能触发 OOM killer - Windows 下默认使用
FILE_MAP_COPY,写操作不落盘,需显式mm.Flush();Linux 下MAP_PRIVATE默认也是 copy-on-write - 不能对
mm.Bytes()返回的切片做append或扩容,会 panic;需确保访问范围在文件实际长度内,越界读不会报错但结果未定义
验证是否真提升:用 go tool pprof 对比 CPU profile 中 syscalls.Syscall 和 runtime.mmap 占比,再看实际耗时——别只信“零拷贝”宣传。
和 bufio.NewReaderSize 搭配使用的边界情况
不是非此即彼。有些场景混合用更稳:比如用 mmap 快速定位某段日志的起始偏移,再用 bufio.NewReaderSize(file).ReadAt 从该偏移开始流式解析行内容。这样既避开 mmap 随机访问的缺页抖动,又避免全量扫描。
注意两个限制:
-
mmap映射后,原*os.File仍可正常使用,但不要同时对同一文件做Write,可能破坏一致性 -
bufio.Reader的ReadAt需要文件支持io.ReaderAt接口,os.File满足,但某些包装器(如gzip.Reader)不满足
真正难处理的是“大行文本”——比如单行 2MB 的 JSON。这时 mmap 可以直接切片,但 bufio.Scanner 默认 64KB 限制会爆,必须提前 scanner.Buffer(make([]byte, 2,反而增加内存压力。这种 case,mmap + 手动找换行符更轻量。











