应优先使用 mmap-go 封装库而非 syscall.Mmap,因其自动处理跨平台映射、页对齐、错误转换、切片长度校验及并发安全卸载,避免 panic 和 SIGBUS,并要求显式 Flush 保证写入持久化。

别用 syscall.Mmap,直接上 mmap-go 封装库——它解决的不是“能不能映射”,而是“映射后不 panic、不 SIGBUS、不丢数据”。
为什么 mmap.Open() 比 syscall.Mmap() 更可靠
Go 标准库压根没提供跨平台 mmap 接口,syscall.Mmap 是裸系统调用封装,不校验页对齐、不转换错误码、不设对切片 len 和 cap。你传个非 4096 对齐的长度,或者文件打开模式和映射权限不匹配,运行时就崩:要么 panic: invalid argument,要么直接 SIGBUS,连堆栈都难定位。
mmap-go(github.com/edsrzf/mmap-go)自动处理这些:
- Linux 调
mmap(2),Windows 调CreateFileMapping+MapViewOfFile,不用你写条件编译 - 强制按 4096 字节页对齐,
f.Stat().Size()多大就映射多大,不会越界 - 返回的
mmap.Map实现了io.ReaderAt和io.WriterAt,可直接当[]byte用,不用unsafe.Slice手动构造 - 内部加锁,支持多 goroutine 并发调
Unmap(),裸syscall.Munmap多次调用直接 crash
只读 vs 写入:权限和文件打开方式必须严格匹配
映射失败八成是因为打开文件的方式和映射标志不一致。这不是 bug,是操作系统硬性要求。
立即学习“go语言免费学习笔记(深入)”;
- 只读映射:用
os.Open("file.bin")+mmap.RDONLY(mmap.Open("file.bin")默认就是) - 写入映射:必须用
os.OpenFile("file.bin", os.O_RDWR, 0)+mmap.RDWR,否则m.IsWritable()返回false,一写就SIGBUS - 想写入并扩大文件?先
syscall.Ftruncate(int(f.Fd()), newSize),mmap不会自动扩容 - 务必在映射前调
f.Stat()拿真实大小,别信配置项、文件名或常量——比如看到文件叫data_2GB.bin就传2 * 1024 * 1024 * 1024,实际只有 1.8GB 就崩
写入后不 Flush() = 数据可能永远不落盘
mmap-go 默认用 MAP_SHARED,修改会反映到文件,但只是进 page cache —— 程序退出、崩溃或断电,没刷过的数据就丢了。这不是库的问题,是 mmap 的语义设计。
- 关键写入后必须显式调
mm.Flush()(等价于msync(MS_SYNC)),否则不保证持久化 - 频繁
Flush()会拖慢性能;批量写完再 flush 是常见折中 - 不要依赖
runtime.SetFinalizer做兜底,finalizer 不保证及时执行,也救不了断电 - 如果只是临时缓存、不强求落盘,得自己调
syscall.Mmap传MAP_PRIVATE,mmap-go不暴露这个选项
大文件随机读写才值得用 mmap,顺序扫描反而更慢
mmap 的优势在于按需分页:你只访问某段偏移,内核才把对应磁盘页载入内存。适合数据库索引查找、日志偏移跳转这类低延迟随机访问。
- 顺序扫描整个大文件(比如做哈希、全文搜索),用
bufio.Reader+ 固定缓冲区(如32 * 1024)更省内存和 CPU - 映射超大文件(>100GB)可能触发内核
vm.max_map_count限制,报错cannot allocate memory,需提前调高该值 - Windows 上注意 64KB 对齐限制,
mmap-go已处理,但自己封装时容易漏 - 映射后 RSS 内存会上升,但不是立即占满——内核按需加载,这点比
os.ReadFile友好得多
真正麻烦的从来不是“怎么映射”,而是“映射后怎么确保每次读写都安全、每次写入都可靠、每次退出都不丢数据”。这些细节藏在页对齐、文件权限、flush 时机和平台差异里,绕不开,也省不得。


















