Go标准库无B+树,需用第三方(如google/btree,实为B-tree)或自研;自研须严格区分内外节点、维护叶子链表、正确处理分裂/合并/删除以保证树平衡。

Go 里没有现成的 B+ 树标准库,得自己实现或选可靠第三方
Go 标准库不提供 BTree 或 BPlusTree,container 包里只有 heap 和 list 这类基础结构。想用 B+ 树做索引,要么集成成熟第三方(比如 github.com/google/btree 虽叫 btree,但实际是 B-tree,非严格 B+),要么自己写——但注意:B+ 树核心在于「所有数据在叶子节点、内部节点只存键和指针」,而多数 Go 的“btree”实现其实是 B-tree(数据可存在内部节点),不满足典型数据库索引语义。
- 如果只是内存内有序映射 + 范围遍历,
github.com/google/btree可用,但要清楚它不存重复键、不保证叶子链表、不分离 key/data 存储 - 若需磁盘持久化或强一致性(如 WAL + 页面刷盘),必须自研或用
badger/boltdb这类底层用 B+ 树(或类似变体)的 KV 引擎,而不是直接暴露树接口 - 自己实现时,优先封装
Page(固定大小字节切片)、Node(区分 internal/leaf)、Tree(含root和minDegree)三层结构,避免泛型早期版本(go )的类型擦除陷阱
插入时分裂逻辑必须处理好叶子与非叶子节点的差异
B+ 树插入不是简单递归下推。叶子节点满时,要拆成两个叶子,并把中位键上提;内部节点满时,也要分裂,但上提的键不带 value,只更新子指针。容易出错的是:没统一处理「上提键归属父节点哪个位置」,或忘了在叶子层维护 next 指针形成双向链表。
- 叶子节点分裂后,新叶子的
next指向原叶子的next,原叶子的next指向新叶子——否则范围查询会断 - 内部节点分裂后,新节点的子指针数组长度比原节点少 1,且首个子指针应继承原节点「被上提键右侧」的子树,否则搜索路径错乱
- 用
bytes.Compare做 key 比较,别用==,尤其当 key 是[]byte时
范围查询依赖叶子链表,不能只靠树遍历
B+ 树的范围查询(如 SELECT * FROM t WHERE id BETWEEN 10 AND 100)性能优势来自叶子节点的有序链表,而非反复回溯树路径。如果只用递归找 min 键再逐个 Next(),但没维护 next 指针,就会退化成 O(n log n)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 实现
Iterator类型时,初始定位到起始 key 所在叶子,然后沿next字段线性扫描,直到超出 end key - 叶子节点需额外字段:
next *LeafNode和prev *LeafNode,插入/分裂/删除时同步更新,删除尤其容易漏掉链表重连 - 如果 key 是复合结构(如
[tenant_id, user_id]),比较函数必须支持前缀匹配,否则Seek(10)可能跳过[10, 5]这样的有效项
删除操作最容易破坏树平衡,必须实现合并(merge)和借位(borrow)
删除不只是移除 key。当叶子或内部节点元素数低于 minDegree - 1 时,必须从兄弟借一个,或与兄弟合并。B+ 树的难点在于:合并叶子节点后,父节点要删掉对应分隔键;而父节点因此不足时,要继续向上合并——这个过程可能一直传到根,导致根变空、高度下降。
- 不要在删除后立刻检查 underflow,应在递归返回时统一处理,否则借位逻辑和合并逻辑混在一起,状态难维护
- 合并两个叶子时,注意拷贝 key/value 数据的同时,也要拼接
next指针链(原右叶子的next成为新叶子的next) - 内部节点合并时,被删掉的分隔键要从父节点移除,且其左右子树指针要合并进新节点的
children数组——这里索引偏移极易算错
真正难的不是写完插入和查询,而是让删除后树依然满足「所有叶子同高」「内部节点键数在 [t−1, 2t−1]」这些约束。调试时多打日志输出 node.keys 和 node.children 长度,比加断点更直观。










