# Bitmasking In Go

> **Source:** [http://localhost:1313/bitmasking/](http://localhost:1313/bitmasking/)
> **Author:** [Vikash Patel](https://vikashpatel.net)
> **Published:** April 17, 2025
> **Reading Time:** 5 min
> 
> *This is the raw Markdown source of the article from the [Lorbic Technical Journal](http://localhost:1313/).*

---


# 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:

```go
1 << 0  // = 1     = 00000001
1 << 1  // = 2     = 00000010
1 << 2  // = 4     = 00000100
1 << 3  // = 8     = 00001000
```

To combine them, we use **bitwise OR** (`|=`):

```go
mask := 0
mask |= 1 << 1  // enable Feature B → mask = 2
mask |= 1 << 3  // enable Feature D → mask = 10
```

So:

```go
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:

```go
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:

```go
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** (`^`):

```go
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          | 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:

```go
// 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)

```json
{
  "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)

```json
{
  "user_id": 123,
  "role_mask": 42
}
```

- `42 = 00101010` → roles 1, 3, and 5 are set.
- Use a simple numeric index:
  ```sql
  CREATE INDEX idx_role_mask ON users(role_mask);
  ```
- Filter with:
  ```sql
  SELECT meta().id FROM users WHERE BITAND(role_mask, 8) = 8;
  ```
  This checks if role 3 is enabled (since `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.

