位图标记大文件需按块粒度映射而非字节一一对应,设块大小4096,则块号blockidx=uint64(offset)/4096,位图容量为向上取整后的块数,须用uint64索引并严格越界检查。

直接用位图标记大文件内容是可行的,但必须把“文件偏移量”映射到“位图索引”,且不能无脑按字节一一对应——否则 1GB 文件就要 125MB 位图内存,得不偿失。关键在粒度控制和边界处理。
位图索引怎么算:别用 offset / 1,改用块粒度
对大文件做标记(比如标出哪些 4KB 块已校验、哪些被跳过),位图不该存“每个字节是否处理”,而应存“每个逻辑块是否完成”。否则内存爆炸,也失去位图优势。
- 设块大小为
blockSize = 4096,则文件第offset字节所属块号是blockIdx := uint64(offset) / blockSize - 位图容量不是
fileSize,而是(fileSize + blockSize - 1) / blockSize—— 向上取整,避免末尾块漏掉 - 若文件 10MB,
blockSize=4096,只需约 2.5K 个 bit(即 313 字节),不是 10MB 个 bit - 切忌用
int存blockIdx:大文件(>2GB)下int可能溢出,统一用uint64
Set 和 Get 必须检查越界,否则读写会静默失败
Go 的 []uint64 不做自动扩容,越界写入不 panic,但会覆盖相邻内存(尤其在小切片或栈分配场景下极易复现);越界读返回 0,容易误判为“未设置”。
- 每次
Set(i)前必须确认i ,否则先扩容:<code>b.bits = append(b.bits, make([]uint64, neededWords)...) -
Get(i)越界时建议返回false(语义上“未标记”),但得文档化说明——不能让调用方误以为“该位置不存在”等于“业务上没发生” - 别依赖第三方库如
github.com/yourbasic/bit的自动扩容:它扩容后新 word 是 0,但你无法区分“这是初始化零值”还是“用户真想标 0”
并发标记同一文件块时,单个 uint64 是共享单元
多个 goroutine 标记不同块,但如果块号映射到同一个 uint64 元素(比如块 100 和块 105 都落在 bits[1]),直接 |= 会触发 data race。
- 最简方案:用
sync.Mutex包住Set方法,适合低并发或块粒度较粗(如 64KB+)场景 - 高并发方案:按
uint64索引分段加锁,例如每 8 个uint64共享一个*sync.RWMutex,减少锁争用 - 千万别用
atomic.OrUint64(&b.bits[wordIdx], mask)来 set 单 bit——mask得是1 ,且必须确保 <code>wordIdx在范围内,否则 atomic 操作本身 panic
序列化位图时,必须存有效长度 lenBits
只把 []uint64 写进文件或网络,反序列化后无法知道“最后几个 bit 是 padding 还是业务数据”。比如 10MB 文件对应 2560 块,但 bits 分配了 2568 块(凑整到 64 的倍数),少了 lenBits 就永远搞不清末尾 8 块该不该信。
- 写入顺序:
binary.Write(w, binary.LittleEndian, uint64(lenBits))→binary.Write(w, binary.LittleEndian, b.bits) - 读取时先读
lenBits,再按需分配b.bits,最后用io.ReadFull保证读完所有字节 - 如果用 JSON 序列化,
lenBits必须作为字段显式导出(如LenBits uint64 `json:"len_bits"`),不能只靠切片长度
真正难的不是实现位运算,而是决定“什么才算一个可标记单元”——是字节、4KB 块、还是自定义 record?选错粒度,位图就从省空间变成拖慢 IO 的累赘。另外,所有越界检查和并发保护都得落在位图封装层里,暴露给上层的接口越干净,后续维护成本越低。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











