Roy Talyosef

Do It With Bitmask

For the first blog, I thought it will be good to start with something simple, but nice. My kind of “Hello World!”. So I figured, why not do a short premier on bitmask? Fun, simple, practical.

What is bitmask? #

Bit mask is a variable that uses bitwise operations to manipulate a single value. It’s a natural way to represent multiple boolean flags or settings.

Each bit in the mask act as a flag. “1” is active, “0” is not active.

There are a few logical operation we can do bit by bit. For example, the “AND” operator will yield 1 if both bits are 1, otherwise 0: 101 & 011 = 001.

So the mask and the operator describes which bits you want to keep, and which you discard.

Why use bit mask? #

Let’s say you’re building a user management system. You could store permissions like this:

type UserPermissions struct {
    CanRead    bool
    CanWrite   bool
    CanExecute bool
    CanDelete  bool
    CanAdmin   bool
    // ... potentially dozens more
}

Or you could use a single integer with bitmasks. Here’s why bitmasks shine:

  1. Memory Efficiency - 64 boolean flags fit in a single uint64 (8 bytes) vs 64 bytes for separate booleans
  2. Performance - Checking perms & ReadFlag != 0 is faster than accessing struct fields
  3. Atomic Operations - You can atomically update multiple flags in concurrent code
  4. Network/Storage - Serializing one integer is simpler than multiple fields
  5. Elegance - user.Grant(Read | Write) is more expressive than setting multiple fields

The trade-off? Bitmasks are less readable for complex logic, but for simple flags and permissions, they’re incredibly powerful.

Bitwise operations in go #

I’m going to give a few practical examples in golang. The should be pretty universal, but might be different in other programming languages.

Operation Operator Description Example
AND & Returns 1 if both bits are 1 01010101 & 01110000 = 01010000
OR | Returns 1 if at least one bit is 1 01010101 | 01110000 = 01110101
XOR ^ Returns 1 if bits are different 01010101 ^ 01110000 = 00100101
LEFT SHIFT << Shifts bits to the left (adds ‘0’ to the right) 01010101 << 2 = 101010100
RIGHT SHIFT >> Shifts bits to the right (removes the lower bits) 01010101 >> 2 = 00010101

The AND NOT Operator (&^) #

Go has a special operator &^ called “bit clear” or “AND NOT”. This is Go-specific syntax that’s incredibly useful for removing specific bits.

a &^ b is equivalent to a & (^b) - it performs AND with the bitwise complement of the second operand.

In practical terms: &^ turns off (clears) the bits that are set in the right operand.

perms := Read | Write | Execute  // 111 (7)
perms &^= Write                  // Clear the Write bit
// Result: 101 (5) - only Read and Execute remain

This is much cleaner than the alternative: perms = perms & (^Write)

Practical examples #

Permissions #

The file permissions in Linux is a classic example of leveraging bit masks operations. If you ever wondered what the number you are passing to chmod, that’s actually octal representation. You can read more in this great article.

rwxrwxrwx
│││││││││
││││││└┴┴─ Others (world) permissions
│││└┴┴─── Group permissions
└┴┴────── Owner (user) permissions

Let’s do something similar, albeit simpler, in golang:

First, define the type. We use uint8 and leverage go’s iota

type Permission uint8

const (
    Read Permission = 1 << iota  // 001 (1)
    Write                         // 010 (2)
    Execute                       // 100 (4)
)
func main() {
	// Give permissions with OR:
	// Can read OR write
	perms := Read | Write

	fmt.Printf("Initial permissions: %03b (%d)\n", perms, perms)

	// Checking if a permission is set using AND
	if perms&Read != 0 {
		fmt.Println("Read permission is enabled")
	}
	// Adding a permission
	perms |= Execute
	fmt.Printf("After adding Execute: %03b (%d)\n", perms, perms)

	// Removing a permission using AND NOT
	perms &^= Write
	fmt.Printf("After removing Write: %03b (%d)\n", perms, perms)

	// Toggling a permission using XOR
	perms ^= Read
	fmt.Printf("After toggling Read: %03b (%d)\n", perms, perms)
}

See it in playground.

The difference from linux is they have 3 sets of permissions group, but shifting or masking correctly will give you the same result.

Feature Flags #

Another nice example is feature flags. Instead of having long if branches, we can just compress it to a single bit mask.

type Feature uint8
const(
	NewGalleryExperience Feature = 1 << iota
)

Using is pretty much the same like the permissions example, so let’s just add convenient functions:


type UserFeatures struct {
	features Feature
}

func (uf *UserFeatures) Enabled(v Feature) bool {
	return v & uf.features
}

func (uf *UserFeatures) Add(features ...Feature) {  
	for _, f := range features {
		uf.features |= f
	}
}

// same goes for Set, Toggle, Clear etc...

Best practices for go #

  1. Use iota. It will automatically create sequential values, perfect for defining bit values. If you store values in persistent storage, you do need to think about removing or changing values, especially true for feature flags.
  2. Choose the appropriate type: uint8, uint16 etc, based on how many values you need.
  3. Design for zero values: In Go, the zero value of integers is 0, which in binary is all bits off. This is perfect for permissions and feature flags because:
type Permission uint8
const (
    Read Permission = 1 << iota  // 1 (001)
    Write                        // 2 (010) 
    Execute                      // 4 (100)
)

// A new user has no permissions by default
var userPerms Permission  // 0 (000) - no permissions

// You must explicitly grant permissions
userPerms |= Read | Write  // Now has read and write

This “secure by default” approach means:

When not to do it in bitmask #

While it’s nice and elegant, sometimes it’s just better to not use bitmasks. I’d suggest avoiding bitmask when you have complex business logic, or when you need more than simple on/off state. Bitmask is also harder to log and debug:

fmt.Printf("User permissions: %d", user.Perms)  // User permission 13 isn't that informative

Also, if you don’t have strict performance requirements, simple struct fields might do the job and are more maintanable when it comes to persistening permissions.

The rule of thumb is: Use bitmasks for simple, performance-critical boolean flags. Use alternative when readability and maintainability are more important than efficiency.

#code