Bitwise and Shift Operators
How to use Go's bitwise and shift operators to manipulate individual bits of integers, with clear examples and common mistakes
Go provides six operators that let you work with the binary representation of integers. Instead of adding or comparing whole numbers, these operators let you inspect, set, or move individual bits. They are fundamental for low‑level programming, cryptography, permissions systems, and any situation where you need to pack multiple flags into a single integer.
All bitwise and shift operators in Go work on integer types only — int, uint, and their sized variants. You cannot use them on floating‑point numbers, strings, or booleans. The result of a bitwise or shift expression is always the same type as the left operand.
Understanding Bitwise Operators
A bitwise operator takes the binary representation of one or two integers and performs a logical operation on each pair of bits. To see how they behave, you only need the truth tables for AND, OR, XOR, and NOT, plus an understanding of how integers are stored as bits.
Go inherits the standard set of bitwise operators from C, with one addition: &^, the bit clear operator. The language also reuses the caret symbol ^ for both XOR and bitwise NOT — context determines which operation you get.
Bitwise AND (&)
The & operator compares each bit of two integers. The result bit is 1 only if both input bits are 1.
| Bit a | Bit b | a & b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
package main
import "fmt"
func main() {
a := 12 // binary 1100
b := 10 // binary 1010
fmt.Printf("%d & %d = %d\n", a, b, a&b) // 12 & 10 = 8 (binary 1000)
}
The bits that are 1 in both numbers remain 1 in the result. For 12 (1100) and 10 (1010), only the bit at position 8 (the third from the right) matches, producing binary 1000, or decimal 8.
AND is often used to check whether a specific bit is set. If you have a flag integer and want to know whether the third bit (value 4) is on, you can test flags & 4 != 0.
Use for Bit Masking:
Bitwise AND is the foundation of bit masking — isolating a set of bits by AND‑ing with a mask that has 1s only in the positions you care about.
Bitwise OR (|)
The | operator sets a bit to 1 if at least one of the corresponding input bits is 1.
| Bit a | Bit b | a | b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
a := 12 // 1100
b := 10 // 1010
fmt.Printf("%d | %d = %d\n", a, b, a|b) // 12 | 10 = 14 (binary 1110)
Every position where either number has a 1 becomes 1 in the result. OR is the natural choice when you want to turn specific bits on without affecting others. For instance, to set the read and write permission bits on a file mode, you would OR the current mode with the combined permission bits.
Bitwise XOR (^)
XOR (exclusive OR) produces 1 only when the two input bits are different.
| Bit a | Bit b | a ^ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
a := 12 // 1100
b := 10 // 1010
fmt.Printf("%d ^ %d = %d\n", a, b, a^b) // 12 ^ 10 = 6 (binary 0110)
Because XOR only flips bits that differ between the two operands, it is perfect for toggling bits. If you XOR a value with a mask that has 1s in the positions you want to toggle, those bits will flip while the rest stay unchanged.
XOR is not a power operator:
In many other languages ^ means exponentiation. In Go, ^ is strictly bitwise. For exponentiation, use math.Pow.
Bitwise NOT (^)
Go does not have a separate ~ symbol for bitwise NOT. Instead, the same ^ operator, when used as a unary operator (with only one operand), acts as a bitwise complement: every 0 becomes 1 and every 1 becomes 0.
a := 5 // binary 0101 (with infinite leading zeros for signed int)
fmt.Printf("^%d = %d\n", a, ^a) // ^5 = -6
Why does ^5 print -6 and not a large positive number? In Go, integers are stored in two’s complement form. The complement of 5 on a 32‑bit signed integer is 11111111 11111111 11111111 11111010, which is the two’s complement representation of -6. If you need a positive bitmask after NOT, use unsigned integers. uint8(^uint8(5)) gives 250.
Watch out for signed integers:
Applying ^ to a signed integer changes its sign. If you want to invert bits and treat the result as an unsigned bitmask, make sure to use an unsigned type.
Bit Clear (AND NOT) (&^)
The &^ operator is specific to Go. It clears (sets to 0) every bit in the left operand where the corresponding bit in the right operand is 1. In other languages you would write a & (~b). Go gives you a &^ b as a single, readable operation.
a := 15 // binary 1111
b := 4 // binary 0100 (the bit to clear)
fmt.Printf("%d &^ %d = %d\n", a, b, a&^b) // 15 &^ 4 = 11 (binary 1011)
Only the third bit (value 4) was cleared; the others remained as they were. This operator makes flag removal explicit and hard to misread.
Express intent clearly:
Using &^ instead of the equivalent a & ^b signals to anyone reading your code that you are deliberately removing bits, not just masking.
Shift Operators
Shift operators move all bits of an integer left or right by a given number of positions. The bits that slide off the edge are discarded, and the vacated positions are filled with zeros or copies of the sign bit, depending on the direction and the signedness of the operand.
Left Shift (<<)
The left shift operator << moves bits to the left, filling the rightmost positions with zeros. Every shift by 1 position multiplies the number by 2.
x := 3 // binary 0011
fmt.Printf("%d << 1 = %d\n", x, x<<1) // 3 << 1 = 6
fmt.Printf("%d << 3 = %d\n", x, x<<3) // 3 << 3 = 24
3 << 3 pushes the binary 0011 three positions left, producing 0011000, which is 24. As a mental shortcut, n << k is n * (2^k) for non‑negative integers.
A common beginner misconception is that the right operand determines how many bits are added. It does not — it specifies how many positions the bits are shifted. New bits on the right are always zero.
Left shift as efficient multiplication:
Left shift is often faster than multiplication by powers of two, though modern compilers usually optimise both to the same machine code. Still, using << for power‑of‑two multiplication makes the intent obvious in low‑level code.
Right Shift (>>)
Right shift moves bits to the right. The vacated positions on the left are filled differently depending on whether the left operand is signed or unsigned.
- For unsigned integers, the shift is logical: zeros fill the left bits.
- For signed integers, the shift is arithmetic: the leftmost bit (the sign bit) is copied to preserve the sign.
var u uint8 = 128 // binary 10000000
fmt.Printf("%d >> 2 = %d\n", u, u>>2) // 128 >> 2 = 32 (binary 00100000, zero-filled)
var i int8 = -8 // binary 11111000 (two's complement)
fmt.Printf("%d >> 2 = %d\n", i, i>>2) // -8 >> 2 = -2 (binary 11111110, sign-extended)
With the unsigned uint8, the two zeros on the left after shifting produce 00100000 (32). With the signed int8, the sign bit 1 is copied, keeping the result negative. -8 >> 2 equals -2, which is consistent with integer division by 2^2 rounding toward negative infinity.
Shift count must be an unsigned integer:
The right operand of a shift must be an unsigned integer (or an untyped constant that fits). Using a signed integer will not compile. If you have an int variable, convert it with uint(n).
Shifting by the bit width or beyond is illegal:
In Go, shifting by a count greater than or equal to the number of bits of the left operand’s type triggers a compile‑time error for constants, or a runtime panic otherwise. For a 32‑bit int, x << 32 is illegal. Always ensure the shift count is less than the bit size.
Signed versus Unsigned Shifts in Practice
The distinction between arithmetic and logical right shift is most visible with negative numbers. If you need a logical right shift on a signed integer, first cast it to an unsigned type, shift, then cast back. This zero‑fills regardless of the sign.
x := int8(-16) // 11110000
unsignedX := uint8(x) // 11110000 interpreted as 240
shifted := unsignedX >> 3 // 00011110 = 30
result := int8(shifted) // positive 30
Common Use Cases
Bitwise operators are not just academic. They appear in real code wherever compact representation or low‑level control is needed.
Flag sets and bitmasks
Store many boolean options in a single integer. Set a flag with flags |= FLAG, clear it with flags &^= FLAG, toggle it with flags ^= FLAG, and test with flags & FLAG != 0.
File permissions in Unix
The os.FileMode type in Go uses bitwise operations heavily. Permission bits like 0777 are combined with OR, and masks check whether a file is readable.
Data packing
When memory is tight, you can store small values in parts of an integer by shifting and masking. Network protocol headers and hardware registers often work this way.
Cryptography and hashing
Many cryptographic algorithms rely on XOR for mixing and on shifts for diffusion. Go’s crypto packages use these operators extensively.
Spotting bitwise operations in real code:
If you see expressions like value & 0xFF, header >> 4, or status |= flag, you are looking at bitwise and shift operators in action. Understanding them lets you read that code with confidence.
Common Mistakes
- Confusing
^as XOR with^as NOT. They are the same symbol, but one is binary (XOR) and the other unary (NOT). The compiler distinguishes them by context, but readers need to pay attention. - Forgetting that right shift on signed integers keeps the sign. A negative
intshifted right remains negative. This can surprise developers used to languages that always zero‑fill. - Using a signed integer as the shift count. It won’t compile. Always use
uintor convert. - Assuming shift is modulo the bit width. In Go it is not; shifting by 32 bits on a 32‑bit value causes a runtime panic. In C it’s undefined behavior, but Go chose safety.
- Treating
&^as a rare oddity. It is actually the cleanest way to express “clear these bits” and is common in Go code. Avoiding it can lead to less readablea & ^bpatterns.
Summary
Bitwise and shift operators give you direct control over the binary representation of integers. Go’s set includes the familiar AND, OR, XOR, and shifts, plus a dedicated bit‑clear operator and a unified ^ for both XOR and NOT. Understanding them opens up efficient flag management, low‑level data manipulation, and the ability to read a huge amount of existing systems code.
The most important practical takeaway: signed right shifts preserve sign, unsigned ones zero‑fill. This one rule prevents a large class of subtle bugs.