Bitmasking in Go
Bitmasking is one of those computer science tricks that feels like wizardry, until you realize it’s just some clever shifting and binary math. This blog explores the idea, shows how we use it in Go, and why it’s surprisingly useful when working with databases like Couchbase.
What’s Bitmasking?
A bitmask is just an integer where each bit (0 or 1) represents a flag or state. Instead of storing multiple booleans in a slice or map, you cram them into a single int. Fast to compute, fast to store, and great for indexing.
Let’s get into what actually happens.
How Bitmasking Actually Works
Let’s say you want to represent a few features:
| Feature Name | Bit Index |
|---|---|
| Feature A | 0 |
| Feature B | 1 |
| Feature C | 2 |
| Feature D | 3 |
We don’t store [true, false, true, false]. We store a single number.
Encoding
To encode which bits are on, we use the left-shift operator 1 << n. This is how we turn a bit position into a power of two.
For example:
11 << 0 // = 1 = 00000001
21 << 1 // = 2 = 00000010
31 << 2 // = 4 = 00000100
41 << 3 // = 8 = 00001000To combine them, we use bitwise OR (|=):
1mask := 0
2mask |= 1 << 1 // enable Feature B → mask = 2
3mask |= 1 << 3 // enable Feature D → mask = 10So:
1Encode([]int{1, 3}) = 10
2// Binary: 00001010Decoding
To get the list of enabled features back, we use bitwise AND (&) to check each bit one by one:
1for i := 0; i < 32; i++ {
2 if mask & (1 << i) != 0 {
3 fmt.Println("Bit", i, "is set")
4 }
5}Why does this work?
Because (mask & (1 << i)) will only be non-zero if bit i is set in mask. For example:
1mask := 10 // 00001010
21 << 3 = 8 // 00001000
310 & 8 = 8 // Bit 3 is set
4
51 << 1 = 2 // 00000010
610 & 2 = 2 // Bit 1 is set
7
81 << 2 = 4 // 00000100
910 & 4 = 0 // Bit 2 is NOT setToggle a Bit
To flip a bit (on → off, or off → on), use bitwise XOR (^):
1mask := 10 // 00001010
2mask ^= 1 << 3 // Bit 3 was 1 → now 0 → mask becomes 2Summary of Operators
| Operator | Meaning | Use Case |
|---|---|---|
1 << n |
Bit at position n |
Build mask |
|= |
Bitwise OR | Set a bit |
& |
Bitwise AND | Check if bit is set |
^= |
Bitwise XOR | Toggle bit (flip) |
The Go Bitmask Package
Here’s the package I wrote with these concepts built in:
1// Encode returns a bitmask with bits set for each ID in the input slice.
2// Each ID is converted using: 1 << id = 2^id
3func Encode(ids []int) int {
4 var mask int
5 for _, id := range ids {
6 mask |= 1 << id
7 }
8 return mask
9}
10
11// Decode returns which bits are set in the mask.
12func Decode(mask int) []int {
13 var ids []int
14 for bit := 0; mask != 0; bit++ {
15 if mask&1 == 1 {
16 ids = append(ids, bit)
17 }
18 mask >>= 1
19 }
20 return ids
21}
22
23// HasBit returns true if the bit at position id is set.
24func HasBit(mask int, id int) bool {
25 return (mask & (1 << id)) != 0
26}
27
28// ToggleBit flips the bit at position id.
29func ToggleBit(mask int, id int) int {
30 return mask ^ (1 << id)
31}Why Use Bitmasking in Couchbase?
Let’s take the example of feature flags or user roles stored in Couchbase.
Option A: Store as Arrays (Bad)
1{
2 "user_id": 123,
3 "roles": [1, 3, 5]
4}Problems:
- Indexing arrays is expensive. Each value creates a separate index entry.
- Couchbase will use
DISTINCT SCAN, which can be heavy. - Filtering with
WHERE ANY r IN roles SATISFIES r = 3 ENDis slow under load.
Option B: Store as Bitmask (Good)
1{
2 "user_id": 123,
3 "role_mask": 42
4}42 = 00101010→ roles 1, 3, and 5 are set.- Use a simple numeric index:
sql
1CREATE INDEX idx_role_mask ON users(role_mask); - Filter with:
This checks if role 3 is enabled (sincesql
1SELECT meta().id FROM users WHERE BITAND(role_mask, 8) = 8;1 << 3 = 8).
Fast, lean, and no complex joins or array scans.
When to Use Bitmasking
Good for:
- Feature flags
- User roles/permissions
- Categorical flags with a known upper limit (< 64 ideally)
- Couchbase performance under large load
Not great for:
- Huge dynamic lists
- Highly descriptive data (bitmasks are not self-documenting)
- Frequent schema changes (bit positions are hard-coded)
Bitmasking isn’t magic; it’s just integers in disguise. When used well, it’s a powerful way to compress and manipulate data for faster storage and filtering, especially in database systems like Couchbase.
If you’re tracking a small set of flags or categories, think in powers of two and let your bits do the work.