不能对原始字节流直接二分查找,因为二进制文件是协议定义的 record 流而非有序数组,字节偏移 i 不对应逻辑记录序号且不保证单调递增;必须先构建 record 起始偏移的 int64 切片索引,再用 sort.Search 在该索引上查找 record 编号,最后通过 file.ReadAt 定位读取。

直接用 sort.Search 配合 io.ReadAt 实现偏移快速定位,但必须把“查找目标”转成偏移索引空间,不能在原始字节流上二分——二进制文件不是有序数组,得先建索引。
为什么不能对 raw bytes 直接二分查找
二进制文件内容是协议定义的 record 流,不是按 key 排序的数组。你没法写 func(i int) bool { return buf[i] >= target }——i 是字节偏移,不是逻辑记录编号,更不保证单调递增。强行二分会越界 panic 或返回完全错误的位置。
- 常见错误现象:
panic: runtime error: index out of range [123456789],因为sort.Search会试探i == len(buf)甚至更大值 - 即使没 panic,返回的
i只是某个字节位置,和你要找的 record 完全无关 - 真正要查的是“第几个 record”,不是“第几个 byte”,必须先有 record → offset 映射
如何构建可二分的 offset 索引
核心是把文件解析成 []int64:每个元素是某条 record 的起始偏移。这个 slice 才是 sort.Search 的合法输入。
- 首次加载时顺序扫描 header(用
binary.Read),记录每个currOffset到 slice,直到 EOF 或超限 - 每条 record 的 offset 必须用
int64存储,避免uint32字段转换溢出 - 索引 slice 不需要存全文,只存前 N 条(比如 10w 条);后续按需增量加载
- 若文件只追加不修改,可把索引缓存到磁盘(如
index.bin),启动时 mmap 加载,跳过扫描
用 sort.Search 查 record 编号再定位 offset
假设你要找 timestamp ≥ 1718400000 的第一条 record,且已有 offsets []int64 和对应 timestamps []uint32(或从 header 动态读):
立即学习“go语言免费学习笔记(深入)”;
- 先二分找 record 下标:
idx := sort.Search(len(offsets), func(i int) bool { return timestamps[i] >= uint32(targetTs) }) - 再校验有效性:
if idx = uint32(targetTs) { ... },否则未找到 - 最后用
file.ReadAt读取:n, _ := file.ReadAt(buf, offsets[idx]),不依赖当前指针 - 注意:不要用
Seek+Read替代ReadAt,否则并发时指针冲突
最容易被忽略的两个点
一是索引构建阶段没做 offset 合法性检查——如果某条 record 的 Len 字段损坏(比如为 0xFFFFFFFF),nextOffset 会溢出成负数,导致后续 ReadAt 直接 panic “invalid argument”,而错误堆栈里根本看不到源头;二是 sort.Search 返回的 idx 没做边界判断就直接当数组下标用,timestamps[idx] 在 idx == len(timestamps) 时 panic,而不是安静地返回 not found。


















