
在 Go 中使用 bitset 位集從基礎操作到高性能集合運算與序列化【免費下載鏈接】lokiLike Prometheus, but for logs.項目地址: https://gitcode.com/GitHub_Trending/lok/loki導讀bitset是 Go 語言生態中用于「非負整數 → 布爾值」映射的高性能位集庫相比map[uint]bool在內存占用與運算速度上均有數量級優勢。本倉庫Grafana Loki以依賴形式引入了github.com/bits-and-blooms/bitsetv1.25.0在布隆過濾器、索引與查詢的位級過濾等場景中作為底層數據結構使用。閱讀本文后你將掌握位集的創建、增刪改查、集合運算、遍歷、序列化與并發安全邊界并能直接在 Go 項目中落地這套方案。位集是什么為什么比map[uint]bool更高效包級文檔對位集的定義非常直白Package bitset implements bitsets, a mapping between non-negative integers and boolean values. It should be more efficient than map[uint] bool.其核心思路是把布爾值按位緊湊地存儲而不是為每個元素單獨分配一個 bool。從源碼看BitSet的內部結構只有兩個字段vendor/github.com/bits-and-blooms/bitset/bitset.go#L86-L90const wordSize 64 const wordBytes wordSize / 8 type BitSet struct { length uint set []uint64 }即底層是[]uint64數組每個 64 位字word可表示 64 個布爾位length記錄當前邏輯位長度。因此「N 位所需內存至少為 N/8 字節」且數組只增長到「最大已設置位的下標 1」對應的字數按需分配extendSet負責擴容。相比map[uint]bool每個條目至少占用一個指針槽位和一個 bool 值通常數十字節位集在密集整型集合場景下內存可縮減一個數量級以上同時得益于math/bits的硬件指令級位運算見popcnt.go中基于bits.OnesCount64的種群計數實現集合基數統計Count與各類集合運算都能以 word 粒度并行推進。Loki 倉庫在 go.mod#L185 中以間接依賴形式引入github.com/bits-and-blooms/bitset v1.25.0位集正是這類日志索引/過濾系統中布隆過濾器等位密集型數據結構的常用底層載體。快速上手安裝與第一個示例安裝方式v1.25.0 對應本倉庫 vendor 目錄中鎖定的版本go get github.com/bits-and-blooms/bitset原文檔給出了一個非常經典的「Go Fish」示例——用它模擬抽牌與配對判斷同時演示Set、Test、Clear三個最基礎的原子操作package main import ( fmt math/rand github.com/bits-and-blooms/bitset ) func main() { fmt.Printf(Hello from BitSet!\n) var b bitset.BitSet // play some Go Fish for i : 0; i 100; i { card1 : uint(rand.Intn(52)) card2 : uint(rand.Intn(52)) b.Set(card1) if b.Test(card2) { fmt.Println(Go Fish!) } b.Clear(card1) } }注意這里var b bitset.BitSet直接使用了零值——文檔與源碼均明確「零值即長度為 0 的空集合」safeSet會在首次使用時自動將set初始化為非 nilbitset.go#L95-L101。創建帶初始容量提示的位集使用bitset.New(length)其實現為make([]uint64, wordsNeeded(length))即按(length63)/64個字預分配避免后續頻繁擴容。此外還有MustNewpanic 版本、From/FromWithLength從既有[]uint64字數組直接構造適合高級用戶零拷貝復用內存等構造函數。核心操作速查設置、清除、翻轉與測試位集對單個整數提供四類最基礎的原子方法且Set、Clear、Flip返回*BitSet支持鏈式調用方法行為返回值Set(i uint)將第 i 位置 1*BitSet可鏈式Clear(i uint)將第 i 位清 0*BitSet可鏈式SetTo(i uint, value bool)按布爾值設置第 i 位*BitSetFlip(i uint)翻轉第 i 位*BitSet可鏈式Test(i uint)測試第 i 位是否為 1boolLen()返回當前位集長度最大下標1uintCount()返回置 1 的位數基數uint從實現看Test通過wordsIndex(i)定位字、uint64(1) (i wordMask)計算掩碼后做與運算Set則先extendSet(i)確保容量足夠再對目標字做或運算。鏈式調用的典型用法b.Set(10).Set(11) // 同時設置第 10、11 位 b.Flip(3).Clear(7) // 翻轉第 3 位再清除第 7 位 if b.Test(10) { // 判斷第 10 位是否被設置 // ... }此外還有面向區間的SetRange(start, end)、FlipRange(start, end)以及全量操作的SetAll()/ClearAll()。針對長度收縮Shrink(lastbitindex)可以按給定下標裁剪位集Compact()則會裁剪尾部多余的零字——文檔明確位集「從不自動收縮」在高頻增刪場景下這兩個方法用于手動歸還內存。遍歷置 1 的位有兩種方式。經典寫法配合NextSet從指定起點向后掃描for i, e : b.NextSet(0); e; i, e b.NextSet(i1) { fmt.Println(The following bit is set:, i) }如果使用 Go 1.23 及以上則可以直接用 range-over-func 語法for i : range b.EachSet() {}EachSet定義在 vendor/github.com/bits-and-blooms/bitset/bitset_iter.go#L19-L31它以bits.TrailingZeros64逐字跳過連續 0 位按升序 yield 每個置 1 位的下標提前 break 會停止迭代。該文件帶有//go:build go1.23構建標簽因此只在 Go 1.23 編譯環境中生效。反向遍歷則可用PreviousSet/PreviousClear。對于需要批量消費的場景NextSetMany(i, buffer)可以一次填充一個[]uint緩沖減少逐位調用開銷。集合運算交集、并集、差集、補集與對稱差位集真正的價值在于把集合運算轉化為 word 級位的按位與/或/異或/取反這是map無法比擬的。完整的方法族如下運算返回新集合返回基數原地修改交集Intersection(other)IntersectionCardinality(other)InPlaceIntersection(other)并集Union(other)UnionCardinality(other)InPlaceUnion(other)差集Difference(other)DifferenceCardinality(other)InPlaceDifference(other)對稱差SymmetricDifference(other)SymmetricDifferenceCardinality(other)InPlaceSymmetricDifference(other)補集Complement()——原文檔中的示例驗證了交集語義if b.Intersection(bitset.New(100).Set(10)).Count() 1 { fmt.Println(Intersection works.) } else { fmt.Println(Intersection doesnt work???) }實現細節上bitset.go#L1010-L1065 附近返回新集合的版本會先按長度對兩個操作數排序再以較短的集合為基準進行位運算從而減少遍歷字數原地版本則把結果寫回調用者。基數版本如IntersectionCardinality不會物化中間結果直接逐字bits.OnesCount64累加適合「只想知道交疊數量」的判斷場景——例如布隆過濾器多塊之間做存在性驗證時只關心交集是否非空。集合查詢方法還包括Any()— 是否存在置 1 的位All()— 是否全部位均為 1None()— 是否沒有任何置 1 的位IsSuperSet(other)/IsStrictSuperSet(other)— 是否為嚴格超集Equal(other)— 兩個位集是否相等Clone()/Copy(c)/CopyFull(c)— 拷貝Copy返回被復制的位數CopyFull保證長度一致Rank(index)/Select(index)— 前 index 位的置 1 計數 / 第 index 個置 1 位的下標見 bitset.go#L1459-L1498是位集上「雙向映射」的經典加速手段DumpAsBits()— 以 0/1 字符串輸出全部位便于調試另一個值得一提的高級接口是Words()替代已廢棄的Bytes()與SetBitsetFrom(buf []uint64)前者直接暴露內部[]uint64字數組非拷貝改動會影響位集后者可用外部字數組就地填充位集兩者均標注「面向高級用戶」可用于零拷貝集成其他位級結構。序列化WriteTo / ReadFrom 與編碼選項位集可以安全、可移植地序列化為字節流。寫入的典型模式原文檔示例const length 9585 const oneEvery 97 bs : bitset.New(length) // Add some bits for i : uint(0); i length; i oneEvery { bs bs.Set(i) } var buf bytes.Buffer n, err : bs.WriteTo(buf) if err ! nil { // failure } // Here n buf.Len()讀取回來// Read back from buf bs bitset.New() n, err bs.ReadFrom(buf) if err ! nil { // error } // n is the number of bytes read從實現看bitset.go#L1332-L1405WriteTo的流格式為先寫一個uint64長度按當前字節序隨后寫wordCount()個 64 位字ReadFrom反向讀取若當前實例容量不足會自動擴展extendSetMaybe并且盡力復用既有實例的內存以減少分配——這正是ReadFrom設計為方法而非構造函數的原因。返回值是寫入/讀取的字節數。關于字節序與編碼包提供了全局配置函數BigEndian()/LittleEndian()/BinaryOrder()— 二進制序列化字節序默認binary.BigEndianBase64StdEncoding()— 切換 JSON 編解碼的 base64 編碼方式默認base64.URLEncoding這兩個開關分別由包級變量binaryOrder與base64Encoding控制bitset.go#L67-L71注意它們是包級全局狀態修改會影響包內所有實例的序列化行為。除io.Writer/io.Reader流接口外位集還實現了標準接口encoding.BinaryMarshalerMarshalBinary/UnmarshalBinary與encoding/jsonMarshalJSON/UnmarshalJSONJSON 形式為 base64 字符串可直接用于json.Marshal與gob等場景。BinaryStorageSize()可預估二進制存儲所需的字節數。性能提示當寫入/讀取目標是文件或網絡連接時建議先用bufio包裝減少系統調用次數f, err : os.Create(myfile) w : bufio.NewWriter(f) f, err : os.Open(myfile) r : bufio.NewReader(f)內存模型與壓縮位集的選擇內存上需要牢記兩個約束N 位的位集至少占用 N/8 字節位集長度始終≥「已訪問的最大位下標 1」——也就是說Set(131)一次就會觸發數 GB 級的擴容。文檔明確警告it is possible to run out of memory while using a bitset。因此對「位稀疏」的大整數集合直接使用bitset可能并不劃算更合適的選擇是壓縮位圖 Roaring bitmap 及其 Go 實現RoaringBitmap/roaring。兩者可相互轉換mybitset : roaringbitmap.ToBitSet() // Roaring - 常規位集 newroaringbitmap : roaring.FromBitSet(mybitset) // 常規位集 - Roaringroaring庫以分段壓縮方式表達稀疏集合在保留集合運算能力的同時大幅降低稀疏場景的內存占用。選型建議位域較密集或下標范圍緊湊時用bitset直接獲得最大吞吐位域稀疏、跨度極大時用 Roaring需要兩者結合時通過上述 API 在運行時互轉。關于 Goroutine 安全文檔明確位集默認不做任何同步跨 goroutine 并發訪問同一實例是不安全的they are unsynchronized for performance。如果確實需要多 goroutine 共享兩種官方建議通道傳遞所有權遵循 Go 慣例通過 channel 把*BitSet在 goroutine 間傳遞保證任意時刻只有一個持有者sync.Mutex串行化用互斥鎖包裹所有對位集的操作犧牲并發換取安全。從源碼看set []uint64的讀寫、extendSet的擴容均未加鎖因此任何形式的并發讀寫包括并發Test都可能造成數據競爭。需要頻繁共享時應優先考慮「每 goroutine 私有位集 周期性合并」的分治模式例如并行分段計算后InPlaceUnion匯總既規避鎖競爭又保留位集運算的高吞吐。測試與驗證原文檔要求提交前運行測試與覆蓋率檢查go test go test -cover本倉庫 vendor 目錄下的位集源碼vendor/github.com/bits-and-blooms/bitset包含bitset.go核心實現約 1800 行、bitset_iter.goGo 1.23 迭代器、select.goRank/Select 支持、popcnt.go種群計數以及pext.gen.go生成的位抽取指令封裝等文件配合倉庫根目錄 go.mod 中鎖定的v1.25.0版本即可復現文檔所述全部行為。位集相關功能在 Loki 中通常位于布隆過濾器、索引結構等存儲路徑如 pkg/storage 下的 bloom/tsdb 相關實現可作為位集在真實大規模日志系統中的應用參考。小結圍繞「非負整數 ? 布爾值」這一核心抽象bitset提供了完整的方法矩陣單點操作的Set/Clear/Flip/Test/SetTo區間與全量的SetRange/FlipRange/SetAll/ClearAll集合層面的交集/并集/差集/補集/對稱差及其基數與原地變體迭代層面的NextSet/NextSetMany/PreviousSet/EachSet序列化層面的WriteTo/ReadFrom/MarshalBinary/MarshalJSON以及內存管理層面的Shrink/Compact/Clone/Copy。將其內化到自己的工具箱你可以在布隆過濾器、位圖索引、權限標記、IP 分配、去重標記等大量位密集型場景中以遠低于map[uint]bool的內存與時間成本完成集合建模與運算?!久赓M下載鏈接】lokiLike Prometheus, but for logs.項目地址: https://gitcode.com/GitHub_Trending/lok/loki創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考