直接用[]bool或map[uint64]bool不够快,因前者内存浪费(1 bool占1字节)、后者哈希开销大且不支持位运算;密集id场景下交并差集慢5–10倍。

为什么直接用 []bool 或 map[uint64]bool 不够快
Go 标准库没有内置 Bitset,但很多人第一反应是用 []bool 或 map[uint64]bool 模拟集合。前者内存浪费严重(每个 bool 占 1 字节,实际只需 1 bit),后者哈希开销大、遍历无序、不支持位运算加速。真正需要交集、并集、差集时,这些结构会拖慢 5–10 倍以上——尤其当元素范围密集(比如 ID 在 0–100000 之间)。
github.com/willf/bitset 的基本用法和坑点
这是最常用的第三方 Bitset 实现,底层用 []uint64 存储,按 64 位打包。但它默认不自动扩容,且 Set() / Clear() 等操作不检查索引越界,容易 panic。
- 初始化必须预估最大位宽:
bs := bitset.New(uint(100000)),传入的是「位数」,不是容量(切片长度由(n+63)/64算出) -
bs.Set(i)中i超过初始化大小会静默失败(不 panic,但无效),建议配合bs.Len()校验 - 交集/并集/差集用
Union()、Intersect()、Difference(),它们返回新实例,原对象不变;若想复用内存,得用UnionAnd()等带And后缀的就地方法 - 遍历推荐用
bs.Each()回调,比手动循环bs.Test(i)快 3–5 倍(跳过全零 uint64 块)
手写轻量 Bitset 的关键逻辑(仅需 50 行)
如果项目不允许引入外部依赖,或只需要子集功能(如只做交集+遍历),自己实现更可控。核心是把位索引 i 映射到数组下标和位偏移:
func (b *Bitset) Set(i uint) {
wordIdx := i / 64
bitIdx := i % 64
if uint(len(b.words))
- 扩容必须用
append扩容[]uint64,不能用make重分配(会丢数据) - 位运算优先级:
1 必须加括号,否则 <code>1 会被解析为 <code>(1 —— 这是对的,但初学者常误写成 <code>1 - 清空单个位用
b.words[wordIdx] &^= 1 (<code>&^是 Go 的位清除操作符) - 交集可直接循环
words数组做&,比逐位判断快两个数量级
性能对比和选型建议
在 100 万元素、稀疏度 1% 的场景下:
-
map[uint64]bool:内存 ~12MB,交集耗时 ~8ms -
github.com/willf/bitset:内存 ~125KB,交集耗时 ~0.03ms(300 倍加速) - 手写 Bitset(无泛型):内存 ~125KB,交集耗时 ~0.02ms,但缺失
String()、JSON 支持等便利方法
真正要注意的是:Bitset 只在「元素值集中且已知上界」时才有优势。如果 ID 是随机 uint64(比如 UUID 哈希后取整),Bitset 内存爆炸,此时老实用 map[uint64]struct{} 更稳妥。











