Go标准库不支持跨平台mmap,直接用syscall.Mmap易panic、泄漏或Windows崩溃;应优先使用github.com/edsrzf/mmap-go,它自动处理页对齐、跨平台映射、资源管理,并提供io.ReaderAt接口和显式Unmap要求。

Go 标准库不支持跨平台 mmap,直接用 syscall.Mmap 极易 panic、泄漏或 Windows 崩溃;真正能落地的方案是用封装好的第三方包,并严格控制映射粒度和生命周期。
为什么不能直接调 syscall.Mmap
它只是裸系统调用封装,不处理页对齐(Linux/macOS 要求 offset 必须是 4096 的整数倍)、不抽象错误码、不管理资源释放,且在 Windows 上根本不可用(返回 ENOSYS)。更危险的是:syscall.Munmap 只接受原始映射返回的 []byte 底层数组,对子切片或 unsafe.Slice 后的指针调用会直接 panic。GC 也完全不感知这块内存,若切片被回收而底层映射未卸载,后续访问触发 SIGBUS 是静默崩溃。
该用哪个 mmap 包:优先 github.com/edsrzf/mmap-go
它自动处理页对齐、跨平台映射(Linux/macOS/Windows 全覆盖)、提供 io.ReaderAt 和 []byte 转换,并强制要求显式 Unmap()。安装只需:
go get github.com/edsrzf/mmap-go
关键行为差异:
-
mmap.RDONLY | mmap.SHARED是只读场景最安全组合,mmap.RDWR需确保文件以os.O_RDWR打开 -
mmap.PRIVATE在只读时可避免意外写入,但 Windows 不支持,慎用 - 映射后得到的
mmap.Map值必须调.Unmap(),且只能调一次——别 defer 到函数末尾就以为万事大吉,若中间提前 return,得手动补Unmap
映射几十 GB 文件 ≠ 安全:按块映射才是正解
一次性 mmap.Map 整个超大文件,虚拟地址空间可能耗尽(尤其 32 位环境),且随机跳读会引发大量缺页中断,磁盘抖动比 os.Read 还差。正确做法是:
- 用
file.Stat().Size()获取真实大小,别信文件名或硬编码长度 - 按固定块大小切分,如
64 (64MB),确保每块起始 <code>offset对齐页边界(offset & (4095) == 0) - 每次只映射当前需要访问的块,读完立刻
mm.Unmap() - 局部读取用
copy(dst, mm[off:off+n]),别把整块[]byte保存为全局变量
示例片段:
mm, err := mmap.Map(file, mmap.RDONLY, 0)
if err != nil {
panic(err)
}
defer mm.Unmap() // 注意:这只卸载当前块,不是整个文件
<p>offset := int64(10 <em> 1024 </em> 1024 <em> 1024) // 第 10GB
n := 1024 </em> 1024
data := make([]byte, n)
copy(data, mm[offset:offset+int64(n)])写入后磁盘没更新?同步和标志位缺一不可
即使用了 mmap.RDWR,若映射时没传 mmap.SHARED,或写完没调 msync,修改永远只在进程内生效。Windows 下尤其容易忽略这点——mmap-go 没暴露 Flush 方法,得自己用 golang.org/x/sys/unix.Msync 或 golang.org/x/sys/windows.FlushViewOfFile。关键点:
- 只读场景绝不要用
mmap.RDWR,权限错会导致mmap.Map失败 - 写入前确认文件系统支持共享映射(NTFS/ext4 可,某些 NFS 或 tmpfs 不行)
- 写完关键数据后立即
msync(mm.Data(), unix.MS_SYNC),别等Unmap——后者不保证刷盘 - 若文件被其他进程
truncate或unlink,你的映射区域会变“幽灵页”,再访问直接SIGBUS
真正难处理的从来不是怎么映射,而是怎么让映射活着的时候不出错、死了之后不残留、改了之后不丢数据——这些细节在日志解析、数据库快照、二进制索引等生产场景里,一个都绕不开。



















