Bitwise Operators

Understand Go's bitwise AND, OR, XOR, NOT, and AND NOT operators, how they manipulate individual bits, and common use cases.

Bitwise operators work on the individual bits of integer values — the 0s and 1s that make up a number’s binary representation. Instead of treating a number as a whole, they compare or change specific bit positions. In Go, these operators let you write code that manipulates permissions masks, packs multiple flags into a single integer, or implements low‑level protocols, all without needing to reach for assembly.

This document covers the five bitwise operators that perform logical operations on bits: & (AND), | (OR), ^ (XOR and NOT), and &^ (AND NOT). Shift operators are treated separately.

The Bitwise AND Operator (&)

a & b returns a new integer where each bit is 1 only when the corresponding bits in both a and b are 1. If either bit is 0, the result bit is 0.

This is the same logic as a truth table: 1 & 1 = 1, 1 & 0 = 0, 0 & 0 = 0.

The operator is often used to check whether a specific bit is set in a value, or to clear bits by ANDing with a mask that has 0 in the positions you want to force to 0.

package main
import "fmt"
func main() {
    var a uint8 = 12 // 00001100
    var b uint8 = 10 // 00001010
    result := a & b
    fmt.Printf("%08b & %08b = %08b (%d)\n", a, b, result, result)
}

The binary of 12 is 00001100, and 10 is 00001010. Only bit 3 is set in both (counting from the right, zero‑based). The output is 00001000, which is 8 in decimal.

A typical scenario is reading a flag bit. If you have a permissions byte where bit 2 means “write access”, you can test permissions & 0b00000100 != 0. This returns true only when that exact bit is 1. The mask 0b00000100 is often written in hex as 0x04.

Don't confuse & with &&:

& is a bitwise operator that returns an integer. && is a logical AND that works with booleans and short‑circuits. Go does not allow you to use & on bool values — the compiler will reject it with a type error.

The Bitwise OR Operator (|)

a | b produces a new integer where each bit is 1 when at least one of the corresponding bits in a or b is 1. The result is 0 only when both bits are 0.

In truth‑table terms: 1 | 1 = 1, 1 | 0 = 1, 0 | 0 = 0.

This operator is the right tool when you need to set certain bits without touching the others. You OR the value with a mask that has 1s in the positions you want to turn on.

var a uint8 = 12 // 00001100
var b uint8 = 10 // 00001010
result := a | b
fmt.Printf("%08b | %08b = %08b (%d)\n", a, b, result, result)
// Output: 00001100 | 00001010 = 00001110 (14)

Bits 1, 2, and 3 end up set because at least one operand had a 1 in those columns. This makes | the natural operator for combining flags. For example, read | write | execute produces a single integer with all three permission bits turned on.

The Bitwise XOR Operator (^)

When used between two operands, ^ is the exclusive OR. a ^ b sets a bit to 1 exactly when the corresponding bits of a and b differ. If the bits are the same, the result bit is 0.

Truth table: 1 ^ 1 = 0, 1 ^ 0 = 1, 0 ^ 0 = 0.

var a uint8 = 12 // 00001100
var b uint8 = 10 // 00001010
result := a ^ b
fmt.Printf("%08b ^ %08b = %08b (%d)\n", a, b, result, result)
// Output: 00001100 ^ 00001010 = 00000110 (6)

Bits 2 and 1 differ between the two operands, so they become 1; bit 3 is 1 in both, so it becomes 0.

XOR is the basis for many symmetric encryption schemes and for swapping two integers without a temporary variable. More practically, you can toggle a bit by XOR‑ing with a mask that has a 1 in that position. value ^ mask flips the bits where the mask is 1 and leaves the rest unchanged.

XOR and the one‑time pad:

XOR is its own inverse: (x ^ y) ^ y gives you back x. This property is why XOR appears in checksums, lightweight ciphers, and the classic “swap two variables” trick.

The Bitwise NOT Operator (Unary ^)

Go uses the same ^ symbol as the bitwise NOT (complement) when it appears before a single operand. ^a flips every bit of a: 0 becomes 1 and 1 becomes 0.

This is equivalent to XOR‑ing the value with an all‑ones mask of the same width. For an unsigned integer type, ^0 gives you that all‑ones constant.

var a uint8 = 12 // 00001100
notA := ^a      // 11110011 in 8-bit representation
fmt.Printf("^%08b = %08b (%d)\n", a, notA, notA)
// Output: ^00001100 = 11110011 (243 for uint8)

The result depends heavily on the type. For uint8, ^12 is 243. For int (which is signed), ^0 is -1 because two’s‑complement representation of -1 has all bits set.

^ on a signed integer can surprise you:

^0 on a signed int yields -1, not a large positive number. If you expect a positive all‑ones mask, use ^uint(0) instead. Misunderstanding this frequently introduces bugs in programs that mix signed and unsigned types.

The AND NOT Operator (&^)

Go provides a dedicated operator for “bit clear”: a &^ b. It returns a value where each bit is 1 only if the corresponding bit in a is 1 and the bit in b is 0. In other words, it clears the bits of a that are set in b.

Mathematically, a &^ b is equivalent to a & (^b), but it’s expressed as a single operator.

var a uint8 = 12 // 00001100
var b uint8 = 10 // 00001010
result := a &^ b
fmt.Printf("%08b &^ %08b = %08b (%d)\n", a, b, result, result)
// Output: 00001100 &^ 00001010 = 00000100 (4)

Bit 3 is set in a but not in b, so it survives. Bit 1 is set in both a and b, so &^ clears it in the result. This operator reads naturally as “a with the bits of b turned off”.

A common application is removing a flag from a bitmask: permissions &^ FLAG clears that flag while leaving the others untouched.

&^ is your tool for unsetting flags:

If you store multiple flags in an integer, mask &^ FLAG is the canonical way to disable a specific flag without disturbing the rest. The code is concise and expresses the intent directly.

Building and Manipulating Bitmasks

The operators above let you pack, unpack, and modify sets of bits. The following table summarises the basic operations for a single bit position using a mask with a 1 in that position.

OperationExpressionEffect
Set a bitvalue | maskForces the bit to 1
Clear a bitvalue &^ maskForces the bit to 0
Toggle a bitvalue ^ maskFlips the bit
Check if bit setvalue & mask != 0Returns true if the bit is 1

The same concepts scale to multi‑bit fields when you construct masks with more than one 1.

package main
import "fmt"
const (
    FlagRead   = 1 << iota // 0001
    FlagWrite              // 0010
    FlagExec               // 0100
)
func main() {
    var perms uint8
    // Set read and exec
    perms |= FlagRead | FlagExec
    fmt.Printf("perms after setting read and exec: %08b\n", perms)
    // Check if write is set
    if perms&FlagWrite != 0 {
        fmt.Println("write is set")
    } else {
        fmt.Println("write is NOT set")
    }
    // Add write
    perms |= FlagWrite
    fmt.Printf("perms after adding write: %08b\n", perms)
    // Remove exec
    perms &^= FlagExec
    fmt.Printf("perms after removing exec: %08b\n", perms)
}

When run, the program prints the permissions byte at each step, showing how the individual flags combine and are later removed. The iota declaration produces the masks 1, 2, and 4, each with a single bit set. The |=, &^=, and &=^ compound assignments mirror the standalone operators.

iota and shift for masks:

The example uses iota together with the left shift operator (<<) to generate powers of two. This pattern is idiomatic for defining flag constants. The shift itself is covered in the next chapter; here it’s only used to construct the masks.

Common Mistakes and Misconceptions

Using bitwise operators on booleans.
Go’s type system prevents bool operands with bitwise operators. Attempting true & false is a compile‑time error. The language forces you to decide between logical operators (&&, ||) and integer‑based bitwise work.

Expecting ^int(0) to be a huge positive number.
For signed types, ^0 is -1 because all bits set is the two’s‑complement representation of negative one. When building masks, use ^uint(0) or explicitly size the type (uint32). This avoids sign‑extension surprises later.

Forgetting that bitwise AND with zero always yields zero.
0 & any_number is 0, which can accidentally wipe out data when you intend to test a bit. Write the check as value & mask != 0, not value & mask == 1.

Confusing &^ with XOR.
a &^ b clears the bits of b in a; a ^ b flips bits where b is 1. The operations are not interchangeable. Toggling a bit twice brings it back, but clearing a bit twice has no second effect.

Precedence can hide bugs:

Bitwise operators bind differently than comparison operators. For example, flags & MASK == 0 is parsed as flags & (MASK == 0), not as (flags & MASK) == 0. Always use explicit parentheses when mixing bitwise operations with comparisons.

Summary

Go’s bitwise operators — &, |, ^, &^, and the unary ^ — give you direct control over the binary representation of integers. Their main job is to build and inspect bitmasks, where each bit signals a distinct on/off property. The dual role of ^ (XOR when binary, NOT when unary) is a unique part of Go’s syntax; remembering this avoids reading ^a as an XOR with a missing operand. The &^ operator, specific to Go, makes clearing flags both readable and efficient.

Bitwise operations appear most often in systems programming, protocol implementations, and any code that packs multiple booleans into a single integer.