推荐优先使用 github.com/edsrzf/mmap-go,因其屏蔽了 linux/macos/windows 差异、api 简洁且经 etcd/bolt 验证;直接手撸 syscall.mmap 易因页对齐、flag 合法性、windows 特殊实现等出错,导致 sigbus 或无效映射。

Go 语言没有内置的跨平台文件内存映射(mmap)支持,直接用 syscall.Mmap 容易出错,推荐优先使用 github.com/edsrzf/mmap-go ——它屏蔽了 Linux/macOS/Windows 的差异,API 简洁,且被 etcd、bolt 等项目长期验证。
为什么不能直接用 syscall.Mmap
手撸 syscall.Mmap 要处理一堆平台细节:页对齐、prot/flags 组合合法性、Windows 上得拼 VirtualAlloc+CreateFileMapping、映射后切片的 len/cap 必须手动设对,否则一读写就 SIGBUS。常见报错 syscall.Mmap: invalid argument 多半是文件长度为 0、没提前 ftruncate、或 PROT_WRITE 和只读文件冲突。
典型坑点包括:
- Linux 下映射只读文件却传
syscall.PROT_WRITE,会静默降级为只读;Windows 下直接失败 - 映射后用
unsafe.Slice或reflect.SliceHeader强转切片时,忘了控制offset + length ≤ mmap 长度,访问越界触发SIGBUS - 忘记调用
syscall.Munmap或mm.Unmap(),导致 fd 泄漏和虚拟内存耗尽
用 mmap-go 打开并读写已有文件
这是最常用场景:加载大日志、索引或二进制数据,避免 os.ReadFile 一次性吃光内存。
示例代码:
file, _ := os.OpenFile("data.bin", os.O_RDWR, 0)
defer file.Close()
fi, _ := file.Stat()
mm, _ := mmap.Map(file, mmap.RDWR, 0) // 映射整个文件
defer mm.Unmap() // 必须调用
data := mm.Bytes() // 底层已正确设置 len/cap,可直接读写
data[0] = 0xff
mm.Flush() // 写回磁盘,非强制但强烈推荐
注意:
-
mmap.RDWR对应PROT_READ | PROT_WRITE+MAP_SHARED,修改会反映到文件 - 若只需读,用
mmap.RDONLY更安全,macOS 还有额外优化 -
mm.Bytes()返回的切片不能append或重切(如data[10:20]可能 panic),长度固定
多进程同时 mmap 同一文件写入会怎样
会错乱,除非加锁。用 MAP_SHARED(mmap-go 默认)时,多个进程看到的是同一份页缓存,但写操作不是原子的——没加锁时,两个进程同时改同一偏移,结果取决于 CPU 指令交错顺序,大概率丢数据。
必须做两件事:
- 用
syscall.Flock或os.File.Chmod配合外部锁机制,在 mmap 前获取独占锁 - 写完调
mm.Flush(),确保修改刷入磁盘;不 flush 的话,程序崩溃或断电就丢了
另外,映射超大文件(>100GB)可能触发内核 vm.max_map_count 限制,报 cannot allocate memory,需调高该值。
随机读写大文件时 mmap 比 ReadAt 快在哪
快在按需分页和零拷贝:你只访问某段偏移,内核才把对应磁盘页载入内存;而 ReadAt 每次都走系统调用+内核缓冲区拷贝。尤其适合频繁跳转访问(如数据库索引查找)。
但要注意:
- 顺序扫描整个大文件时,
bufio.Reader+Read更省内存和 CPU - 频繁小写(比如每字节改一次)反而更慢——缺页中断开销太大
- 映射后文件被其他进程截断或删掉,下次访问对应地址会
SIGBUS
真正容易被忽略的是:flush 不是免费的,批量写完再 flush 是常见折中;别依赖 runtime.SetFinalizer 做兜底,它不保证及时执行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











