How to Use Bitwise Operators in Go

Use Go's bitwise operators like &, |, and << to manipulate individual bits in integer values for efficient flag management and data processing.

Bitwise operators for flags, not math

You're building a permission system for a file manager. Each file needs read, write, and execute rights. You could use three boolean fields. That works. But the C library you're wrapping expects a single integer for permissions. Or you're parsing a binary network protocol where bits pack flags tightly to save bandwidth. You need to toggle individual bits inside an integer without disturbing the others. Bitwise operators let you do exactly that.

Think of an integer as a row of light switches. Each switch represents a bit. The rightmost switch is the least significant bit. Bitwise operators let you inspect or flip specific switches based on rules. & checks if two switches are both on. | turns a switch on if either input is on. ^ flips a switch if exactly one input is on. Shifts move the switches left or right. Go adds &^ to turn off bits.

Bitwise operators treat integers as collections of bits. Use them to manipulate flags, not to do math.

The core operators

Here's the full set of bitwise operators in action.

package main

import "fmt"

func main() {
	// Binary literals make the bit patterns readable.
	a := 0b1100 // 12
	b := 0b1010 // 10

	// AND keeps bits set only where both inputs have 1.
	andResult := a & b

	// OR sets bits where either input has 1.
	orResult := a | b

	// XOR sets bits where inputs differ.
	xorResult := a ^ b

	// Shift left multiplies by two, pushing bits toward higher values.
	leftShift := a << 1

	// Shift right divides by two, dropping bits off the end.
	rightShift := a >> 1

	// AND NOT clears bits in a that are set in b.
	andNotResult := a &^ b

	fmt.Printf("AND: %04b\n", andResult)
	fmt.Printf("OR:  %04b\n", orResult)
	fmt.Printf("XOR: %04b\n", xorResult)
	fmt.Printf("LSH: %05b\n", leftShift)
	fmt.Printf("RSH: %04b\n", rightShift)
	fmt.Printf("ANDN:%04b\n", andNotResult)
}
# output:
AND: 1000
OR:  1110
XOR: 0110
LSH: 11000
RSH: 0110
ANDN:0110

The operators work on the binary representation. The decimal value is just a side effect.

How the operators behave

0b1100 is 12. 0b1010 is 10.

& compares bit by bit. 1100 & 1010 yields 1000. Only the third bit is set in both inputs.

| merges bits. 1100 | 1010 yields 1110. Any bit set in either input ends up set in the result.

^ finds differences. 1100 ^ 1010 yields 0110. Bits where the inputs match cancel out. Bits where they differ stay set.

Shifts move bits. << 1 shifts left by one position, effectively multiplying by two. >> 1 shifts right, dividing by two.

Go has a unique operator: &^. This is AND NOT. a &^ b clears bits in a that are set in b. It's equivalent to a & (^b), but Go provides the combined form to make intent clear and avoid precedence traps.

Go uses ^ for both XOR and bitwise NOT. When you write a ^ b, it's XOR. When you write ^a, it's NOT. The compiler figures it out from the number of operands. This saves a key on the keyboard but trips up beginners expecting a tilde.

The bitwise NOT operator ^a flips every bit. On a signed integer, this produces the two's complement negation minus one. For example, ^5 on a 32-bit int yields -6. This behavior comes from how negative numbers are stored. Bitwise NOT is rarely useful on signed types. Use it on uint where the result is predictable. ^uint(5) gives 0xFFFFFFFA.

Go distinguishes signed and unsigned integers. Right-shifting a signed integer performs an arithmetic shift, filling with the sign bit. Right-shifting an unsigned integer fills with zero. Use uint types for bitwise work to keep the behavior predictable and avoid sign extension bugs. The Go community prefers uint for bitmasks. Signed integers carry sign bits that can interfere with shifts and comparisons. If you see int in a bitwise context, ask why. There's usually a reason, but uint is the default choice.

Go's &^ operator saves you from writing a & (^b). Use it to clear bits cleanly.

Real-world flag systems

Here's how bitwise operators power flag systems in real code.

package main

import "fmt"

// Permission defines bit positions for file access rights.
// Each value is a power of two so bits don't overlap.
const (
	Read  uint = 1 << iota // 001
	Write                  // 010
	Execute                // 100
)

// HasPermission checks if the mode contains the required flag.
// The bitwise AND isolates the target bit, then we compare.
func HasPermission(mode, flag uint) bool {
	return (mode & flag) == flag
}

func main() {
	// Combine flags using OR to build a permission set.
	userMode := Read | Write

	fmt.Println("Can read?", HasPermission(userMode, Read))
	fmt.Println("Can exec?", HasPermission(userMode, Execute))

	// Add execute permission by OR-ing the new flag.
	userMode |= Execute

	fmt.Println("Can exec now?", HasPermission(userMode, Execute))

	// Remove write permission using AND NOT.
	userMode &^= Write

	fmt.Println("Can write now?", HasPermission(userMode, Write))
}
# output:
Can read? true
Can exec? false
Can exec now? true
Can write now? false

The iota constant generator is the standard way to define bit flags in Go. It increments automatically, so 1 << iota produces 1, 2, 4, 8, and so on. This guarantees each flag occupies a unique bit position. The flag package in the standard library relies on this pattern for command-line parsing.

When checking flags, always compare the result of & against the flag value. Writing mode & flag evaluates to a non-zero integer, which is truthy in C but not in Go. Go requires an explicit boolean comparison.

Flags make code dense. Document your bit positions so future readers don't have to reverse-engineer the magic numbers.

Common bit manipulation patterns

Bitwise operators enable compact tricks for specific tasks. Wrap these in named functions to keep the calling code readable.

package main

import "fmt"

// IsPowerOfTwo returns true if x is a power of two.
// A power of two has exactly one bit set.
// Subtracting one flips all bits below that position.
// AND-ing the result with the original yields zero.
func IsPowerOfTwo(x uint) bool {
	return x > 0 && (x&(x-1) == 0)
}

// ToggleBit flips the bit at position n.
// XOR with a mask of 1 at position n inverts that bit.
func ToggleBit(x uint, n int) uint {
	return x ^ (1 << n)
}

// ExtractBit returns the bit at position n as 0 or 1.
// Shift right moves the target bit to position zero.
// AND with 1 masks out everything else.
func ExtractBit(x uint, n int) uint {
	return (x >> n) & 1
}

func main() {
	fmt.Println("IsPowerOfTwo(8):", IsPowerOfTwo(8))
	fmt.Println("IsPowerOfTwo(6):", IsPowerOfTwo(6))

	val := uint(0b1010)
	fmt.Printf("ToggleBit(0b1010, 1): %04b\n", ToggleBit(val, 1))
	fmt.Println("ExtractBit(0b1010, 1):", ExtractBit(val, 1))
}
# output:
IsPowerOfTwo(8): true
IsPowerOfTwo(6): false
ToggleBit(0b1010, 1): 1000
ExtractBit(0b1010, 1): 1

Bit tricks are fast. Readability is faster. Comment your bit hacks or wrap them in named functions.

Pitfalls and compiler errors

Operator precedence causes subtle bugs. << has higher precedence than ==. Writing x & mask == 0 is parsed as x & (mask == 0). The compiler rejects this with invalid operation: x & mask == 0 (mismatched types int and bool). Wrap the bitwise operation in parentheses: (x & mask) == 0.

The shift amount must be an unsigned integer or an untyped constant. If you try to shift by a signed variable, the compiler rejects it with invalid operation: shift of type int by type int. Convert the shift amount to uint or use a constant.

Shifting by the width of the type or more causes a compile error. The compiler complains with constant 1 << 64 overflows uint if you try to shift a 64-bit integer by 64 positions. Validate shift amounts at runtime if they come from user input.

Parentheses save you from precedence bugs. Use them liberally around bitwise expressions.

When to use bitwise operators

Use bitwise operators when you need to pack multiple boolean flags into a single integer for storage or network transmission. Use bitwise operators when you are interfacing with C libraries or hardware protocols that expect bitmasks. Use bitwise operators when you need to toggle individual bits efficiently without allocating memory. Use a struct with boolean fields when readability matters more than memory density and you don't need bitwise manipulation. Use a map[Key]bool when the set of flags is dynamic or sparse and you don't know the keys at compile time. Use plain boolean variables when you have a fixed, small number of flags and want the code to be self-documenting.

Bitwise operators are tools for packing and toggling. Reach for them when the problem is about bits, not when you're forcing a square peg into a bitmask hole.

Where to go next