- 开发工具
【免费下载链接】go-datastructures
A collection of useful, performant, and threadsafe Go datastructures.
go-datastructures 是一个收集了多种常用、高性能且面向并发(threadsafe)Go 数据结构的开源库,覆盖区间树、位数组、队列、范围树、斐波那契堆、集合、切片扩展等十余个模块。本文以 documentation.md 为纲,结合仓库源码逐一剖析各数据结构的核心设计、适用场景与底层实现,帮助读者在选型时快速判断"该用哪个包、为什么、怎么用"。
项目定位与设计目标
正如 documentation.md 开头所述,这个库的目标有两层:
- 移植:把其他语言中一些常见数据结构的成熟实现移植到 Go;
- 改进:在已有数据结构之上做出针对 Go 语言特性的优化。
这些结构被设计为社区可复用的公共组件,希望借助开源汇集更多人的工程经验来持续打磨 Go 侧的实现。仓库根目录的 datastructures.go 用一个空导入包统一导入了全部子包,方便 depman 等依赖管理工具只导入这一个包即可获得所有子包;go.mod声明模块为github.com/Workiva/go-datastructures(go.mod)。
获取、测试与上手
从 README.md 的 Installation/Testing 小节可以确认标准的获取与验证方式(要求 Go 1.3+,仓库go.mod当前声明go 1.15):
# 获取全部子包 go get github.com/Workiva/go-datastructures/... # 首次拉取测试依赖并运行全部单元测试 go get -t -u ./... go test ./... # 之后每次只需 go test ./...下文每一节给出的源码路径都对应仓库中真实存在的文件,读者可直接对照阅读。
Augmented Tree:n 维区间相交查询
区间增广树(Augmented Tree)专门用于判定区间之间的相交关系。文档给出的典型场景是电子表格:把单元格 A1 表示为半开区间 A1:B2,然后遍历"图"去查找精确或近似的相交关系。仓库包注释则用二维平面举例:由(0,0)与(10,10)围成的平面是否与(-5,-5)与(5,5)围成的平面重叠(答案是重叠)——把点查询也建模成长度为零或极小的小区间即可统一处理。
接口与实现要点
- Interval 接口:任何要放入树中的元素都必须实现 Interval 接口,提供
LowAtDimension(uint64) int64、HighAtDimension(uint64) int64、OverlapsAtDimension(Interval, uint64) bool和唯一ID() uint64。注释明确要求区间不可变——返回值一旦变化会导致未定义行为。 - Tree 接口:Tree 提供
Add(...Interval)、Delete(...Interval)、Query(Interval) Intervals与Traverse(func(Interval))。用接口而非具体类型返回,是因为构造出的树可能是单维或多维的。 - 构造器:
augmentedtree.New(dimensions uint64) Tree(atree.go)。
从 atree.go 的实现看,底层是一棵自顶向下的红黑二叉搜索树(node结构体带red标志位,旋转/双旋转逻辑见 rotate / doubleRotate)。每个节点额外维护子树内所有区间的min/max边界(setMin / setMax),查询时据此剪枝:
- 插入路径上的节点会同步更新
max/min(add),保证 O(log n) 级别的单维操作; - 删除后通过
adjustRanges自底向上重算边界(Delete); - 查询沿树下行时,先用
overlaps(n.children[i].max, high, n.children[i].min, low)判断子树是否有相交可能,再逐节点调用intervalOverlaps(query)。多维相交判定正是依赖Interval.OverlapsAtDimension在维度 2..maxDimension 上逐一比对(intervalOverlaps)。
维度代价与未来方向
文档明确指出:当前实现支持 n 维,但最快的查询是能"在第一维上完成过滤"的查询;如果查询只能靠第一维以外的维度过滤,速度会明显变慢。因此使用时尽量把主过滤维度放在第一维。Query返回的Intervals结果列表复用了sync.Pool缓冲并支持Dispose()归还(intervals.go),高频查询场景记得调用Dispose以减少分配。文档中的 TODO 是补充自底向上版本以更好地处理重复区间。
Bit Array:用位运算加速集合比较
位数组(bitarray/bitmap)适用于"数据可表示为整数 ID"的集合比较场景。文档举的例子是:集合{1, 3, 5}与{3, 5, 7}各自编码进位数组后,一次按位与即可完成交集计算(不计算编码/解码开销),而不必逐个元素比较。包内还提供把位数组还原为整数列表的能力。
两种密度实现
仓库的 BitArray 接口 定义了SetBit/GetBit/ClearBit/Reset/Capacity/Count/Intersects/Or/And/Nand/ToNums等方法,接口背后有两种实现:
- 稠密实现(dense):
bitArray以[]block为底层存储(bitarray.go 同包文件 bitarray.go),位操作直接在整型块上完成,速度最快; - 稀疏实现(sparse):借鉴线性代数中压缩行稀疏矩阵(CSR)的思想,把大范围稀疏的位图压缩存储。代价是插入与存在性检查退化为 O(log n),但求交集时仍保有速度优势(文档原话:retains some speed advantages when checking for intersections)。
两种实现都要求元素拥有uint64唯一标识,因此它很适合"知道 ID 但不想用哈希表判存在"的场景——这一点在 README.md 的 Bitarray 小节有更直白的描述。
常量长度 Bitmap
此外包内还有 Bitmap32 / Bitmap64:分别把 32/64 个布尔值直接塞进一个uint32/uint64,所有操作都是 O(1) 的纯位运算(SetBit/ClearBit/GetBit/PopCount),无需数组、无需哈希。
文档还顺带点明:位数组加上 B+ 树,是构建原生 Go 数据库的两块基石之一;这个"Going Forward"的线索在文档结尾再次出现。
Futures:一次完成、全员广播的结果
Futures 解决的是"一个 goroutine 开始某项操作后,希望多个等待方都能收到同一个结果"的问题。用缓冲 channel 也能做,但对同一结果要反复发送给每个等待者,显得有些冗余。仓库包注释(futures.go)说得更直白:channel 在多个监听者同时监听时只会随机挑选一个消费者,而 Future 会把结果广播给所有监听者,并缓存结果,后来的监听者立刻拿到。
核心 API 与实现
// Completer 是一个只读 channel,Future 只从上面收结果 type Completer <-chan interface{} // 构造:监听 completer,超时后所有等待者收到 error func New(completer Completer, timeout time.Duration) *Future // 阻塞直到结果就绪,然后返回 (item, err) func (f *Future) GetResult() (interface{}, error) // 非阻塞地判断结果是否已产生 func (f *Future) HasResult() bool实现上,Future结构体(futures.go)用sync.Mutex保护triggered/item/err,用sync.WaitGroup挂起所有等待者;listenForResult在select中同时监听 completer channel 与time.Timer,超时则把所有监听者以timeout after %f seconds错误唤醒(listenForResult)。由于只完成一次、wg.Done()只调一次,天然满足"广播"语义,且结果被缓存后GetResult直接返回、不再阻塞。
行为上它类似于 GAE 中 ndb 的 futures,可被理解为一种"广播 channel"。使用注意点:每个 Future 只能完成一次,不要在同一个 completer 上重复调用New。
Queue:队列、优先队列与无锁环形缓冲
queue 包内含三种队列,文档与包注释(queue.go)说明其共同特性是发送永不阻塞、按需无限增长,且对已Dispose的队列继续 Put 会返回 error 而不是像向已关闭 channel 发送那样 panic——这更贴合 Go 官方"用 error 表达失败"的设计理念。
普通队列与 waitgroup 协调
普通队列用waiters []*sema挂起等待者(waiters),空队列上等待的 goroutine 会在收到消息时被唤醒;调用Dispose会立刻让所有等待线程带着 error 返回,可用于优雅地通知消费者退出。文档特别指出:只要给出合理的构造 hint(容量提示),普通队列在向 goroutine 发送数据时比 channel 更快。包注释中附有当时环境的基准测试(BenchmarkQueue-8 671 ns/op对比BenchmarkChannel-8 2083 ns/op),可作为参考而非绝对结论。
优先队列
priority_queue.go 要求元素实现Item接口的Compare(other Item) int,返回 1/0/-1 表达大于/等于/小于(Item)。内部是一个用切片实现的二叉堆:pop把末叶换到根后逐层"下沉"恢复堆序(priorityItems.pop)。文档提示当前优先队列速度一般,理想替代是斐波那契堆(仓库中已有 fibheap 可对照)。
MPMC 无锁环形缓冲
queue 包最近还加入了多生产者多消费者(MPMC)环形缓冲RingBuffer:容量向上取整到 2 的幂(roundUp,ring.go),并用 padding 隔离queue/dequeue计数器避免伪共享。满时 Put、空时 Get 会阻塞,但Dispose会让所有被阻塞的线程带着 error 返回——这正是文档强调的"同步并安全退出 goroutine,便于对象被 GC 回收"的用法。线程安全完全依靠CAS 原子操作,因此速度很快(包注释基准BenchmarkRBPut-8 58.1 ns/op、BenchmarkRBGet-8 26.8 ns/op)。其设计参考了 1024cores 的 bounded MPMC queue。文档列出的 Future 计划是进一步实现无锁环形缓冲。
Range Tree:n 维点数据的区间查询
范围树用于以对数级复杂度存储并查询n 维整数点(通常建模为笛卡尔坐标)。注意两点:
- 查询区间含端点(inclusive);
- 虽然名字里有 Tree,但从包注释(rangetree/interface.go)和实现看,仓库实际采用的是稀疏 n 维有序数组而非传统 BBST——文档说明这是因为 n 维场景下稀疏数组实现更简单、更高维时更省空间。
两种实现
- 可变版本:
orderedTree(orderedtree.go)以顶层有序节点列表逐维下钻,插入复杂度为 O(log n^d)(d 为维度,README 给出)。速度快,但若消费者需要线程安全,写入侧会有锁竞争; - 不可变版本:
immutableRangeTree(immutable.go)是写时复制(copy-on-write)结构——写操作只复制路径上的那部分节点,其余子树被共享。如 Add 所示,每次添加返回一棵新树,旧树保持不变,因此多线程读写互不阻塞。代价是单次操作更慢,且批量写入时性价比最高。
两者都通过Entry.ValueAtDimension(uint64) int64定位点在每一维的值,并通过Interval描述查询范围(Entry / Interval / RangeTree)。RangeTree 接口还提供了Apply(按序迭代命中项、可提前取消)、Get(按地址批量取回)与InsertAtDimension(批量平移某维度上的元素并返回被移动/被删除的条目)等实用操作。文档的 TODO 是统一两套实现到同一接口并进一步优化性能。
Fibonacci Heap:摊销 O(1) 的 decrease-key
斐波那契堆(fibheap)是标准实现,键为浮点优先级。它的价值在于decrease-key 操作摊销 O(1),因此非常适合作为 Dijkstra 最短路径与 Prim 最小生成树算法的底层优先队列,达到理论最优复杂度。包注释(fibheap.go)给出了经典结论:insert/peek/merge 为常数级,decrease-key 摊销 O(1),dequeueMin/delete 摊销 O(log n);以 Dijkstra 为例,用斐波那契堆可做到 O(m + n log n),优于二叉堆/二项堆的 O(m log n)。
实现上,堆以满足最小堆性质的环形双向链表组织多棵树的森林:每个节点记录父指针、任意一个子指针、度数(孩子数)与标记位,堆本身持有最小值指针;dequeueMin在弹出最小根后把其子节点并入根表,再按度数合并树,保证根表中每种度数至多一棵树;decreaseKey通过"剪切-上浮-级联标记"恢复堆性质,delete则退化为"把键降到 -∞ 再 dequeueMin"。
实践提醒(README 亦有说明):斐波那契堆常数因子较大,实际性能未必优于配对堆,且未针对线程安全设计——单线程/单 goroutine 使用为宜。文档的 Future 计划包括:为每个节点挂一个interface{}值指针以携带用户数据、用本堆写出快速 Dijkstra/Prim 实现、以及分析并设计线程安全变体。
Set:基于 map 的线程安全无序集合
文档对集合模块着墨不多:这是一个由 Go map 支撑的无序集合,且是线程安全的——加锁对性能略有影响,但读操作可以并行。查看 set/dict.go:
- 内部是
items map[interface{}]struct{}加sync.RWMutex(Set):Add/Remove走写锁,Exists/Len/All走读锁,因此并发读不互斥; Flatten会把元素摊平成切片并缓存,下次 Flatten 直接返回缓存;- 构造器
New(items ...interface{})从sync.Pool取对象复用内存(init),Dispose会清空并归还池子。
用法上元素类型是interface{},方法集较小(Add/Remove/Exists/Flatten/Len/Clear/All/Dispose)。文档的 Future 方向是:基于标准库hash/fnv(非常快的非加密哈希)从零实现哈希表,并尝试无锁哈希表。
Slice:Int64 版的有序切片
Go 标准库sort包自带IntSlice([]int)的排序/搜索辅助,但由于早期 Go 没有泛型,int64 版本必须单开包。slice 包就是标准库IntSlice的int64 对位实现(slice/int64.go),并且额外增加了一个Insert方法。
Int64Slice实现sort.Interface(Len/Less/Swap),并提供:
Sort():原地排序;Search(x int64) int:二分查找,返回不低于 x 的最小下标(调用方需自行判断该位置是否恰为 x,Search);Exists(x):基于 Search 的判存在,未排序时行为未定义;Insert(x):把 x 插入已排序切片中的正确位置并返回新切片,若已存在则原样返回(Insert)。
Int64Slice还被 rangetree 内部用作维度坐标缓存(见 immutable.go 的newCache),是库内复用度较高的基础件。
Threadsafe:锁包装的线程安全组件
threadsafe 包的思路很直接:给一些常见接口加锁,使其可被多 goroutine 并发访问。文档为此辩护道:虽然 Go 官方建议"忘掉锁、用 channel",但 channel 本质上是锁的高级抽象,且在某些"多 goroutine 共同写一块内存"的场景里,channel 既丑又慢又没必要。
目前包内唯一的实现是 threadsafe/err 的Error结构:用sync.RWMutex保护一个error字段,提供Set(err error)与Get() error,方便多 goroutine 并发地向同一个错误变量写入(比如并行任务的第一个错误)。注意该包注释已标记Deprecated,建议改用golang.org/x/sync/errgroup或atomic.Pointer[error]——新代码可优先考虑这些替代方案。
结语与未来方向(Going Forward)
文档结尾勾勒了作者的长期规划:仓库已存在 B+ 树相关 PR(本仓库 btree 目录下已有 palm、plus、immutable 等变体),B+ 树 + 位数组这两块拼图齐备后,就有条件写一个原生的 Go 数据库。这也是为什么文档中"位数组是构建原生 Go 数据库所需的两件东西之一"的伏笔在这里回收。
对使用者的建议同样写在文档里:这些数据结构是大量程序与算法(即使它们常被上层库抽象隐藏)的地基,欢迎任何人提交优化与 bug 修复。阅读完本文后,你可以按需深入各子包源码——例如区间查询场景看 augmentedtree、集合运算看 bitarray、并发广播看 futures、goroutine 间协调看 queue、n 维点查询看 rangetree、图算法底层层看 fibheap——每处都有对应的_test.go测试文件可以对照验证行为。
- 开发工具
【免费下载链接】go-datastructures
A collection of useful, performant, and threadsafe Go datastructures.
相关推荐
二叉堆与斐波那契堆性能对比:数据结构选择的终极指南
二叉堆与斐波那契堆性能对比:数据结构选择的终极指南 在算法和数据结构的世界中,堆与优先队列是实现高效操作的关键工具。今天我们将深入探讨两种重要的堆实现:二叉堆和
示例工程Floccus跨浏览器书签同步完整操作手册:打造你的私有书签云
Floccus跨浏览器书签同步完整操作手册:打造你的私有书签云 在当今多设备、多浏览器的数字生活中,书签同步已成为现代互联网用户的核心需求。Floccus作为一
前端移动开发数据同步《程序员数学:斐波那契》——为什么不能用斐波那契散列做数据库路由算法?
《程序员数学:斐波那契》——为什么不能用斐波那契散列做数据库路由算法? 导读:斐波那契数列不只是面试题里的递归经典案例,它在 JDK 源码中催生了 Thread
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考