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:
1 << 0 // = 1 = 00000001
1 << 1 // = 2 = 00000010
1 << 2 // = 4 = 00000100
1 << 3 // = 8 = 00001000
To combine them, we use bitwise OR (|=
):
mask := 0
mask |= 1 << 1 // enable Feature B → mask = 2
mask |= 1 << 3 // enable Feature D → mask = 10
So:
Encode([]int{1, 3}) = 10
// Binary: 00001010
Decoding
To get the list of enabled features back, we use bitwise AND (&
) to check each bit one by one:
for i := 0; i < 32; i++ {
if mask & (1 << i) != 0 {
fmt.Println("Bit", i, "is set")
}
}
Why does this work?
Because (mask & (1 << i))
will only be non-zero if bit i
is set in mask
. For example:
mask := 10 // 00001010
1 << 3 = 8 // 00001000
10 & 8 = 8 // Bit 3 is set
1 << 1 = 2 // 00000010
10 & 2 = 2 // Bit 1 is set
1 << 2 = 4 // 00000100
10 & 4 = 0 // Bit 2 is NOT set
Toggle a Bit
To flip a bit (on → off, or off → on), use bitwise XOR (^
):
mask := 10 // 00001010
mask ^= 1 << 3 // Bit 3 was 1 → now 0 → mask becomes 2
Summary of Operators
Operator | Meaning | Use Case |
---|---|---|
1 << n |
Bit at position n |
Build mask |
` | =` | Bitwise OR |
& |
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:
// Encode returns a bitmask with bits set for each ID in the input slice.
// Each ID is converted using: 1 << id = 2^id
func Encode(ids []int) int {
var mask int
for _, id := range ids {
mask |= 1 << id
}
return mask
}
// Decode returns which bits are set in the mask.
func Decode(mask int) []int {
var ids []int
for bit := 0; mask != 0; bit++ {
if mask&1 == 1 {
ids = append(ids, bit)
}
mask >>= 1
}
return ids
}
// HasBit returns true if the bit at position id is set.
func HasBit(mask int, id int) bool {
return (mask & (1 << id)) != 0
}
// ToggleBit flips the bit at position id.
func ToggleBit(mask int, id int) int {
return mask ^ (1 << id)
}
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)
{
"user_id": 123,
"roles": [1, 3, 5]
}
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 END
is slow under load.
Option B: Store as Bitmask (Good)
{
"user_id": 123,
"role_mask": 42
}
42 = 00101010
→ roles 1, 3, and 5 are set.- Use a simple numeric index:
CREATE INDEX idx_role_mask ON users(role_mask);
- Filter with:
This checks if role 3 is enabled (since
SELECT 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.