WAL日志必须追加写+Sync,禁用bufio.Writer:安全写法是用os.O_CREATE|os.O_RDWR|os.O_APPEND打开文件,固定record格式,每条写完立即file.Sync(),否则断电或kill-9会导致数据丢失。

跳表只存 offset/length,别存整行内容
跳表在 Go 里没有标准库实现,但真正卡住性能的从来不是结构选型,而是 value 存了什么。map[string]string 存完整日志行?2GB 文件轻松吃掉 5GB 内存,GC 频繁触发,查询变慢。正确做法是:map[string][]struct{ offset int64; length int },key 用时间戳字符串或哈希前缀(保证可排序),value 只记位置信息。
构建时注意三点:
- 先调
os.Stat().Size()预估总行数,避免[]struct{...}频繁扩容 - 插入前用
sort.Search定位插入点,维持 slice 有序,后续二分查找才快 - 查询后必须走
file.Seek(offset, io.SeekStart)+io.ReadFull(file, buf[:length]),别依赖bufio.Scanner—— 它的底层数组会被复用,读出来的可能是上一次残留数据
倒排索引必须配行偏移数组,不能裸用 map[string][]int
map[string][]int 是倒排起点,但它只告诉你“第几行”,不告诉你“这一行从哪开始”。查到行号 lineNo 后,没 lineOffset[lineNo] 就没法 Seek 到磁盘位置。
构建 lineOffset []int64 的关键细节:
立即学习“go语言免费学习笔记(深入)”;
- 每读完一行,立刻调
file.Seek(0, io.SeekCurrent)记下当前偏移,别用scanner.Bytes()算长度——换行符差异(\nvs\r\n)会导致偏移错位 - 数组长度必须和实际行数一致,
lineOffset = make([]int64, totalLines+1),留出哨兵位避免越界 - 中文分词必须用
github.com/go-ego/gse并传seg.WithFrequency(false),否则每个词多带频次字段,内存多占 30%+
suffixarray 只适合静态大文本高频查,别每次新建
index/suffixarray 不是通用加速器。用户每次输新关键词就调一次 suffixarray.New(data)?构建耗时 20μs 起,比 strings.Index 的 30ns 慢 700 倍。
它只在三个条件同时满足时值得用:
- 文本完全不变(如协议定义、日志模板)
- 长度 >10KB
- 后续搜索 ≥200 次
正确用法:启动时对只读文件构建一次 *suffixarray.Index,之后所有查询复用它;其余场景,老老实实 strings.Index 更快更省心。
WAL 日志必须追加写 + Sync,禁用 bufio.Writer
本地 KV 引擎崩溃后数据不一致,90% 出在这儿:WAL 没真正落盘。os.WriteFile 或 bufio.Writer 包裹的文件句柄,断电或 kill -9 就丢最后几条。
安全写法只有这一种:
- 打开文件用
os.O_CREATE | os.O_RDWR | os.O_APPEND - record 格式固定:
uint32(len(key))+ key 字节 +uint32(len(value))+ value 字节 - 每条写完必须立刻调
file.Sync(),这行不能省,也不能被缓冲绕过
最易被忽略的是:WAL replay 时,遇到损坏 record(比如长度字段异常)必须跳过,不能 panic,否则重启失败;而跳过的代价,是那条写入永远丢失——所以 WAL 是“尽力可靠”,不是“绝对不丢”。



















