映射前必须预分配文件大小,否则 mmap 写越界会 panic;日志索引需用 ftruncate 预设长度,零拷贝应直接操作 m.Bytes() 定位字段并存偏移,避免字符串复制;并发扫描须用 atomic.Uint64 同步偏移。

映射前必须预分配文件大小,不能依赖 os.Create 自动扩容
直接 os.Create 一个空文件再 mmap,后续写入时若没提前 ftruncate,会因映射长度 > 文件当前大小而 panic。日志索引场景下,你通常知道最大容量(比如按天滚动、固定 10GB),必须在 mmap 前完成预分配:
- 用
os.OpenFile打开文件,权限设为os.O_CREATE | os.O_RDWR - 调
syscall.Ftruncate(int(f.Fd()), size)(Linux/macOS)或SetEndOfFile(Windows)确保文件长度到位 - 再传给
mmap.Open或mmap.Map,否则mmap.RDWR映射失败是常态
别指望 mmap 自动拉伸文件——它只映射已有内容,写越界触发 SIGBUS 而非错误返回。
索引构建时别用 string 或 []byte 复制原始行内容
内存映射的核心价值是零拷贝,但很多人在构建 Trie 或倒排索引时仍习惯 scanner.Text() → string → 提取关键词 → 存 offset,这完全浪费 mmap。正确做法是直接操作映射切片:
- 用
m.Bytes()获取整个映射视图,配合bytes.IndexByte/bytes.FieldsFunc定位换行符和字段边界 - 关键词提取用
unsafe.String(&m[i], j-i)(需确保 i,j 在映射范围内)或直接存[2]uint64{i, j}偏移对 - Trie 叶子节点只存
[]int64(字节偏移列表),不存任何字符串副本
这样整条索引链路无堆分配,GC 压力趋近于零,10GB 日志索引构建内存峰值可压到 200MB 以内。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
多 goroutine 并发扫描同一 mmap 区域,无需读锁但要注意偏移同步
纯读场景下,多个 goroutine 同时访问 m.Bytes() 是安全的——底层页表共享,无竞争。但索引构建常需记录“已扫到哪”,这时容易踩坑:
- 别用普通
int变量做扫描位置计数器,竞态导致跳行或重复扫 - 用
atomic.Uint64管理当前偏移,每次取值后用atomic.AddUint64推进 - 注意:
m.Len()返回的是映射长度,不是文件实时大小;若文件被其他进程截断,atomic值可能超出有效范围,需加if pos 防御
实际中建议每 1MB 扫描块做一次原子检查,比全程加锁快 3–5 倍,且不会因某 goroutine 卡住拖慢全局。
写入索引结构后必须显式 Flush,defer 不管用
用 mmap 构建的索引最终要落盘(比如生成 .idx 文件),但 mm.Unmap() 或 defer mm.Close() 不会刷脏页——修改仍留在 page cache,断电即丢:
- 构建完索引数据后,立刻调
mm.Flush()(等价于msync(MS_SYNC)) - 若索引结构本身也走 mmap(如预分配的 offset 数组),则需在每个逻辑段写完后 flush,而非只在最后调一次
- Windows 下
mmap-go的Flush()会调FlushViewOfFile,行为一致,不用额外处理
最容易忽略的是:flush 前没确认 m.IsWritable(),尤其当映射用 mmap.RDONLY 打开却误写,SIGBUS 直接 kill 进程,连 recover 都捕获不到。

















