以下是一份关于 btree 包中这段 Go B-Tree 泛型实现(BTreeG[T]) 的详细解析。代码相对完整且较为复杂,融合了 Copy-On-Write、PathHint(路径提示加速)、锁(可选)、bulk load、iter 等众多机制。为方便理解,本解读将分为若干部分,并对核心结构与关键函数做逐步阐述。
目录:
- 整体概览
- 数据结构:
BTreeG[T],node[T]等 - 核心成员与初始化
- 查找相关:
bsearch/find/hintsearch - 路径提示(
PathHint) - 插入/删除
- Copy-On-Write 机制
- 锁相关
- 迭代器(
IterG[T]) - 其他操作:如
GetAt,DeleteAt,Load,PopMin/PopMax,Min/Max…
BTreeG[T] 是一个支持泛型的 B-Tree 数据结构,提供了如下特性:
- 常规 B-Tree:插入、删除、查找、迭代等操作,仍保持 O(log N)。
- Copy-On-Write (COW):当克隆 (
Copy()/IsoCopy()) 一棵树时,并不会复制所有节点,而是在写操作时“懒复制”节点,节省内存与时间。 - Path Hints:针对多次访问相近 key 的情况,可以传入
PathHint来跳过二分搜索的一部分操作,提升速度。 - 并发安全:可配置是否使用
sync.RWMutex(locks = true时启用),从而在多协程下使用。
核心类型大致包括:
BTreeG[T]:表示一棵泛型 B-Treenode[T]:树的节点,存储items切片和可选的children数组PathHint:存储路径提示,用于加速查找Options:配置项,比如Degree、NoLocksIterG[T]:迭代器
type BTreeG[T any] struct {
isoid uint64 // 树的“隔离ID”,用于COW判断
mu *sync.RWMutex // 读写锁
root *node[T] // 根节点
count int // 元素总数
locks bool // 是否启用锁
copyItems bool // 是否需要在插入/复制时对 items 做深拷贝
isoCopyItems bool // 是否需要特殊的 IsoCopy
less func(a, b T) bool
empty T // 零值
max int // 最大可容纳的items (2*degree - 1)
min int // 最少要求的items
}isoid:通过newIsoID()生成,用于区分不同“副本”之间的节点归属(Copy-On-Write 的核心)。root:指向根节点,若为空表示树空。count:当前树中所有元素的数量。max, min:与 B-Tree 的最小度数 (degree) 相关,决定每个节点最多/最少能存储的元素数。less(a, b T) bool:比较函数,用来在插入/查找时做排序判定。
type node[T any] struct {
isoid uint64 // 节点所属的隔离ID, 和BTreeG[T].isoid对应
count int // 该节点(含子节点)下所有元素的数量
items []T // 节点内存放的元素(有序)
children *[]*node[T] // 指向子节点的列表(如果是内部节点)
}leaf():若children == nil,表示叶子节点。
type PathHint struct {
used [8]bool // 记录深度小于8的节点是否已经使用hint
path [8]uint8 // 存储每层的索引位置
}- 最多记录 8 层深度(对绝大部分实际 B-Tree 已足够)。
- 在搜索时可优先使用 hint 里的 index,而不是做二分。
func NewBTreeG[T any](less func(a, b T) bool) *BTreeG[T] {
return NewBTreeGOptions(less, Options{})
}
func NewBTreeGOptions[T any](less func(a, b T) bool, opts Options) *BTreeG[T] {
tr := new(BTreeG[T])
tr.isoid = newIsoID()
tr.mu = new(sync.RWMutex)
tr.locks = !opts.NoLocks
tr.less = less
tr.init(opts.Degree)
return tr
}
func (tr *BTreeG[T]) init(degree int) {
if tr.min != 0 {
return
}
tr.min, tr.max = degreeToMinMax(degree) // 转换degree到min/max
// 检查 empty 这个零值是否是 copier[T]/isoCopier[T] (可选)
...
}degreeToMinMax(deg int) (min, max int):度数 -> 最少/最多存储的元素数量。tr.init(0)在没有指定 degree 时会默认32。
func (tr *BTreeG[T]) newNode(leaf bool) *node[T] {
n := &node[T]{isoid: tr.isoid}
if !leaf {
n.children = new([]*node[T])
}
return n
}- 分配一个节点,若不是叶子,创建
children切片。 isoid与 B-Tree 相同,用于判断是否需要 copy-on-write。
func (tr *BTreeG[T]) bsearch(n *node[T], key T) (index int, found bool) {
low, high := 0, len(n.items)
for low < high {
mid := (low + high) / 2
if !tr.less(key, n.items[mid]) {
low = mid + 1
} else {
high = mid
}
}
if low > 0 && !tr.less(n.items[low-1], key) {
return low - 1, true
}
return low, false
}- 标准的二分搜索,用
tr.less(key, n.items[mid])做比较。 - 如果最终
low > 0 && !less(n.items[low-1], key)说明n.items[low-1] == key,返回(low-1, true)。
- 如果没有
hint,直接调用bsearch。 - 如果传了
hint,则进入hintsearch优先尝试 hint 中的索引,再做局部二分,最后更新 hint。
func (tr *BTreeG[T]) hintsearch(n *node[T], key T, hint *PathHint, depth int) (index int, found bool) {
// 如果hint已记录本层路径, 尝试 index = hint.path[depth] 处做对比
// 若不匹配, 则在局部 [low, high] 范围做二分
// ...
// 最终返回 (index, found)
// 并更新 hint.path[depth] = pathIndex
}- 可以减少一次完整二分,从而加速若多次访问相邻 key。
type PathHint struct {
used [8]bool
path [8]uint8
}- 在多次操作(例如
SetHint,GetHint,DeleteHint)中传入同一个PathHint。 - 树会更新这条提示,使其对下次访问更加精准。
- 如果出错了,依旧会 fallback 到二分搜索。
func (tr *BTreeG[T]) SetHint(item T, hint *PathHint) (prev T, replaced bool) {
if tr.locks { tr.mu.Lock(); defer tr.mu.Unlock() }
return tr.setHint(item, hint)
}
func (tr *BTreeG[T]) setHint(item T, hint *PathHint) (prev T, replaced bool) {
if tr.root == nil {
// 树为空,初始化root
...
}
prev, replaced, split := tr.nodeSet(&tr.root, item, hint, 0)
if split {
// root节点满了, 需要分裂root
left := tr.isoLoad(&tr.root, true)
right, median := tr.nodeSplit(left)
tr.root = tr.newNode(false)
*tr.root.children = []*node[T]{left, right}
tr.root.items = []T{median}
tr.root.updateCount()
// 再次调用setHint保证插入真正落到分裂后的合适节点
return tr.setHint(item, hint)
}
if replaced {
return prev, true
}
tr.count++
return tr.empty, false
}nodeSet:递归插入到节点n中,如果n需要分裂就返回split = true。- 如果最终
root也split了,就创建一个新的根节点,把left、right分裂结果挂上。
func (tr *BTreeG[T]) nodeSet(cn **node[T], item T, hint *PathHint, depth int) (
prev T, replaced bool, split bool,
) {
// 1. copy-on-write if needed
if (*cn).isoid != tr.isoid {
*cn = tr.copy(*cn)
}
n := *cn
// 2. 查找插入位置 i
i, found := ...
if found {
// 替换旧值
prev = n.items[i]
n.items[i] = item
return prev, true, false
}
// 3. 若是叶子
if n.leaf() {
if len(n.items) == tr.max {
return tr.empty, false, true // 需要split
}
// 直接插入
n.items = append(n.items, tr.empty)
copy(n.items[i+1:], n.items[i:])
n.items[i] = item
n.count++
return tr.empty, false, false
}
// 4. 若非叶子,先递归到子节点
prev, replaced, split = tr.nodeSet(&(*n.children)[i], item, hint, depth+1)
if split {
// 分裂子节点后再把“中间key”提到本节点
if len(n.items) == tr.max {
return tr.empty, false, true
}
right, median := tr.nodeSplit((*n.children)[i])
...
// 递归继续插入到自身
return tr.nodeSet(&n, item, hint, depth)
}
if !replaced {
n.count++
}
return prev, replaced, false
}func (tr *BTreeG[T]) DeleteHint(key T, hint *PathHint) (T, bool) {
if tr.lock(true) { defer tr.unlock(true) }
return tr.deleteHint(key, hint)
}
func (tr *BTreeG[T]) deleteHint(key T, hint *PathHint) (T, bool) {
if tr.root == nil { return tr.empty, false }
prev, deleted := tr.delete(&tr.root, false, key, hint, 0)
if !deleted { return tr.empty, false }
if len(tr.root.items) == 0 && !tr.root.leaf() {
tr.root = (*tr.root.children)[0]
}
tr.count--
if tr.count == 0 {
tr.root = nil
}
return prev, true
}delete会在节点找出key位置,若是叶子,则直接移除;若是内部节点,需要找前驱/后继替换,或者合并子节点(nodeRebalance)。- 同样需要在子节点元素数低于
min时做合并或向兄弟节点借元素。
func (tr *BTreeG[T]) isoLoad(cn **node[T], mut bool) *node[T] {
if mut && (*cn).isoid != tr.isoid {
*cn = tr.copy(*cn)
}
return *cn
}- 在修改(
mut == true)节点前,会检查节点isoid是否与当前树isoid一致,不一致时执行tr.copy(*cn)创建副本并赋予新的isoid。 tr.copy(n *node[T]) *node[T]做了一个浅层拷贝:复制items,若copyItems或isoCopyItems为真,则对每个 item 调用Copy()或IsoCopy()。
func (tr *BTreeG[T]) Copy() *BTreeG[T] {
return tr.IsoCopy()
}
func (tr *BTreeG[T]) IsoCopy() *BTreeG[T] {
if tr.lock(true) { defer tr.unlock(true) }
tr.isoid = newIsoID() // 改变自身isoid
tr2 := new(BTreeG[T])
*tr2 = *tr
tr2.mu = new(sync.RWMutex)
tr2.isoid = newIsoID() // 新树也给一个新的isoid
return tr2
}- 当调用
IsoCopy()时,会先更新当前树的isoid(防止后续操作也要写时复制),再复制整个BTreeG结构给tr2,然后给tr2一个新isoid。 - 两个树共享
root节点指针,但isoid不同,后续修改会触发 copy-on-write。
locks表示是否需要加锁。lock(write bool)/unlock(write bool):- 如果
locks为true, 在写操作时加mu.Lock(), 读操作加mu.RLock()。
- 如果
type IterG[T any] struct {
tr *BTreeG[T]
mut bool
locked bool
seeked bool
atstart bool
atend bool
stack0 [4]iterStackItemG[T]
stack []iterStackItemG[T]
item T
}
type iterStackItemG[T any] struct {
n *node[T]
i int
}- 维护一个栈,从根节点一路往下,记录访问过的节点和 index,用来支持
Next()/Prev()操作。
iter := tr.Iter() // 或 tr.IterMut()
defer iter.Release()
ok := iter.First()
for ok {
item := iter.Item()
...
ok = iter.Next()
}Seek(key)/SeekHint(key, hint):从root开始查找key并将位置记录到 stack 中。Next():- 若是叶节点,就
i++,如果越界则回溯父节点; - 若不是叶子,则进入下一子节点的最左分支。
- 若是叶节点,就
Prev()同理反方向。
GetAt(index)/DeleteAt(index):支持按序号访问。通过n.count来定位是去哪个子节点,或命中n.items[i]。PopMin()/PopMax():弹出最小/最大。Load(item T):bulk load 的一种方式,用于往末尾追加插入时做快速处理。Walk()/Scan():遍历整棵树,Walk是一次性批量传入回调的 slices;Scan一边调用iter(item)一边返回。Min()/Max():去最左/最右路径拿到最小/最大值。
- 数据结构概念
BTreeG[T]+node[T]实现 B-Tree 基础;isoid+isoLoad+copy(...)实现了 Copy-On-Write。
- 路径提示 (
PathHint)- 用于跳过常规二分搜索的一部分,若访问 key 分布相近,可大幅加速。
- 锁
- 通过
locks开关决定是否用sync.RWMutex,满足多协程访问需求。
- 通过
- 插入/删除
- 标准 B-Tree 做法,节点满了就分裂 (split),节点不足就合并 (rebalance)。
- 在插入/删除/修改时,若节点不属于当前
isoid,则执行 copy-on-write。
- 迭代器
- 保存从根到当前位置的节点索引栈
stack,实现Next()/Prev()。
- 保存从根到当前位置的节点索引栈
- Copy-On-Write
- 核心在
isoLoad(),若(*cn).isoid != tr.isoid并且是写操作,会复制节点。 IsoCopy()使得整棵树复制仅仅是“新分配了一个 BTreeG 结构”,节点本身暂不复制,直至后续有写操作发生时复制节点。
- 核心在
这份代码融合了多种实用技巧,适合在读多写少或局部写、并发场景下使用。通过 PathHint、COW、锁、迭代器 等手段,提供了一个功能相当完善且高性能的泛型 B-Tree 实现。