Go中不用[]bool而用手写位图,因其底层每个bool占1字节,100万布尔值需1MB;位图1位存1值,同等数量仅约125KB,可避免缓存与GC压力。

为什么不用 []bool 而要手写位图
Go 的 []bool 底层是按字节存储的,每个 bool 占 1 字节 —— 存 100 万个布尔值就占 1MB;而位图用 1 位存 1 个值,同样数量只需约 125KB。这不是“省点内存”的问题,而是当你要处理千万级开关状态(比如布隆过滤器、权限位、稀疏标记)时,[]bool 会直接把缓存和 GC 拖垮。
常见错误现象:make([]bool, 1 看似轻量,实测分配耗时高、GC 压力明显,且无法做原子批量操作(比如一次性置位 64 个 bit)。
- 位图本质是
[]uint64(或[]uint32),靠位运算读写单个 bit - 索引
i对应的字(word)位置是i / 64,位偏移是i % 64 - 必须自己处理越界检查 — Go 不像 Rust 有
BitVec这类带边界保护的 std 类型
Set 和 Get 的正确位运算写法
别用 & 1 直接判断 — 它在 <code>i >= 64 时行为未定义(Go 中左移超过类型位宽是 0)。必须先取模再移位。
示例:对 bits []uint64 操作第 i 位
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func (b *Bitmap) Set(i uint) {
wordIdx := i / 64
bitIdx := i % 64
if uint(len(b.bits)) func (b *Bitmap) Get(i uint) bool {
wordIdx := i / 64
bitIdx := i % 64
if uint(len(b.bits))
1 是安全的,因为 <code>bitIdx已被限制在[0,63]- 不要用
int做索引变量 — 32 位系统下int可能溢出,统一用uint或uint64 -
Get对越界索引返回false是合理设计,但得明确文档化,否则调用方可能误以为“不存在”等于“未设置”
如何避免 Clear 时清错位或漏清
清除位容易犯两个错:一是用 &^= 1 但没校验 <code>i 是否越界;二是想批量清零一段范围时,直接循环调用 Clear — 性能差一个数量级。
- 单 bit 清除必须先确认
wordIdx在范围内,否则b.bits[wordIdx] &^= ...会 panic - 清空连续区间
[start, end):先算出起始/结束 word 索引,对中间整块用0赋值,首尾残块用掩码(mask)逐 bit 清 - 注意:Go 的
uint64是小端序,但位序是标准数学序(bit 0 是最低位),掩码构造别反了 — 比如清bit 3~7,掩码是0x78(二进制000001111000),不是0x1E
内存对齐与并发访问的坑
[]uint64 天然按 8 字节对齐,适合 atomic 操作,但不能直接对单个 bit 做原子读写 — Go 的 atomic 包只支持整字长操作(Uint64、Uint32 等)。
- 如果多个 goroutine 同时改不同 bit,但落在同一个
uint64字上,就会发生“伪共享”(false sharing),性能骤降 - 解决方案:要么加
sync.Mutex(简单但重),要么按 cache line 对齐(每 64 字节放一个uint64,中间 padding),或者用atomic.Or64/atomic.And64配合掩码实现无锁 set/clear(需确保 mask 无竞争) - 别忘了:
len(b.bits)不是总 bit 数,真实容量是uint64(len(b.bits)) * 64— 这个换算在 resize 和序列化时极易出错
位图真正难的不是写对四个函数,而是想清楚“谁在什么条件下会并发读写哪几个字”,以及“扩容时旧数据怎么迁移不丢 bit”。这些细节一漏,线上就出现偶发的 false negative。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










