应优先使用 github.com/edsrzf/mmap-go 而非 syscall.Mmap,因其自动处理跨平台兼容、页对齐、错误转换及安全切片构造;syscall.Mmap 返回切片 len/cap 为 0,易 panic 或 SIGBUS,且不校验权限与范围,Windows 不可用。

别直接调 syscall.Mmap,它会 silently crash
Go 标准库没提供安全跨平台的 mmap 接口,syscall.Mmap 是裸系统调用包装,不校验页对齐、不转换错误码、不处理 Windows 兼容性,更糟的是:它返回的 []byte 默认 len 和 cap 都是 0。你直接 len(mm) 或 mm[100] 就 panic;用 unsafe.Slice 手动构造切片时若 offset + length 超出映射范围,访问即触发 SIGBUS —— 进程静默退出,无堆栈、难复现。
真正可用的路径只有两条:
- Unix 类系统(Linux/macOS):用
golang.org/x/sys/unix.Mmap,它类型安全、错误码可判,但仍需手动对齐offset(必须是 4096 倍数)、校验文件真实大小(不能硬写1024*1024*1024) - 跨平台或快速落地:直接上
github.com/edsrzf/mmap-go,它自动处理页对齐、平台差异、len/cap设置,并统一暴露mm.Bytes()和mm.Flush()
mmap-go 怎么开箱读写,但写完文件还是空?
常见错觉:改了 mm.Bytes()[0] = 1,立刻 ls -l 或用其他进程读,发现没变。这不是 bug,是 mmap 的缓存语义使然。
关键点:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
mmap.OpenFile("x.dat", os.O_RDWR, 0)默认用MAP_SHARED,修改内存会反映到文件,但只进 page cache,不立即落盘 -
defer mm.Close()只做munmap,**完全不刷盘**;断电或崩溃,数据就丢了 - 必须显式调
mm.Flush()(等价于msync(MS_SYNC)),才强制同步到磁盘 - 如果只是临时解压/计算中间态,想避免刷盘开销,得退回
syscall.Mmap并传MAP_PRIVATE—— 但mmap-go不暴露该 flag
映射 10GB 文件却 runtime: out of memory?不是物理内存不够
报错不是因为 RAM 不足,而是虚拟地址空间耗尽(尤其在 32 位环境或 Go runtime 内存碎片严重时)。一次性 mmap 整个超大文件,可能让 Go 无法为后续 goroutine 分配新栈或堆对象。
正确做法是按块映射:
- 每块大小建议 64MB–128MB(
64 * 1024 * 1024),确保起始offset & 4095 == 0 - 处理完一块后立刻
mm.Unmap(),释放对应 vma 区域 - 用
unix.Madvise(mm.Bytes(), unix.MADV_DONTNEED)提示内核丢弃已用页,降低 RSS 占用 - 永远先
f.Stat().Size()拿真实长度,别信文件名或硬编码 size
随机跳转快,顺序扫全文件反而更慢?
mmap 的优势只在特定访问模式下成立:频繁跳转读写固定偏移(如日志索引查找、数据库 WAL 解析)、重复扫描同一区域。它靠“按需加载”省去拷贝,但代价是缺页中断开销。
不适合的场景:
- 顺序读整个大文件:此时
bufio.NewReaderSize(f, 1或io.Copy更高效,系统能预读、合并 IO - 跨度大、局部性差的随机访问:缺页中断频发,性能反低于
os.File.ReadAt - 频繁小写(如逐字节改):每次写都触发缺页,比
WriteAt慢得多
最易被忽略的一点:mmap 不解决磁盘 I/O 瓶颈,只优化数据搬运路径;若底层存储慢(如机械盘、网络文件系统),再怎么 mmap 也快不起来。

















