Bitwise operators
Bitwise operators rarely show up in day-to-day Go application code. You will meet them when reading the standard library, working with binary protocols, or dealing with permission and flag values, but you usually will not write them yourself for a while. If this lesson feels dense on a first pass, skim it and come back later. Nothing in upcoming chapters depends on a deep grasp of bitwise.
The operators from the previous lesson work on whole values. Bitwise operators work on the individual 0s and 1s that make up an integer, one position at a time. If you have never used them, they look unusual at first, so it is worth going slowly.
Every integer is stored in memory as a sequence of bits. Each bit position has a place value, a power of two. The rightmost bit is worth 1, the next is worth 2, then 4, 8, 16, doubling as you move left. To read a binary number in decimal, add up the place values of the positions that contain 1:
| Bit position | 3 | 2 | 1 | 0 | Decimal |
|---|---|---|---|---|---|
| Place value | 8 | 4 | 2 | 1 | |
0b1100 |
1 | 1 | 0 | 0 | 8 + 4 = 12 |
0b1010 |
1 | 0 | 1 | 0 | 8 + 2 = 10 |
Bitwise operators take two such numbers, line them up bit by bit, and combine them one position at a time to produce a new integer.
AND, OR, and XOR
Three of the operators work like the logical &&, ||, and "exclusive or" you know from booleans, except they apply to each bit position independently:
fmt.Println(0b1100 & 0b1010) // 8 (0b1000)
fmt.Println(0b1100 | 0b1010) // 14 (0b1110)
fmt.Println(0b1100 ^ 0b1010) // 6 (0b0110)
To see how they combine our two example numbers position by position, read each column of this table top to bottom:
| Bit position | 3 | 2 | 1 | 0 | Decimal |
|---|---|---|---|---|---|
| Place value | 8 | 4 | 2 | 1 | |
0b1100 |
1 | 1 | 0 | 0 | 12 |
0b1010 |
1 | 0 | 1 | 0 | 10 |
& (AND) |
1 | 0 | 0 | 0 | 8 |
| (OR) |
1 | 1 | 1 | 0 | 14 |
^ (XOR) |
0 | 1 | 1 | 0 | 6 |
At bit position 3, both inputs are 1, so AND is 1, OR is 1, XOR is 0. At bit position 2, only the first input has a 1, so AND is 0, OR is 1, XOR is 1. Continue for every position and you get the three result rows.
Each operator has a specific job when you are manipulating a set of flag bits packed into one integer:
&tests whether a bit is set, or keeps only certain bits and zeroes the rest.|sets bits, without touching the ones already on.^toggles bits on or off.
AND-NOT (&^)
Go has one bitwise operator you may not recognise from other languages. a &^ b keeps each bit of a where b has a 0, and clears the bit wherever b has a 1:
fmt.Println(0b1100 &^ 0b1010) // 4 (0b0100)
The same bit-by-bit view:
| Bit position | 3 | 2 | 1 | 0 | Decimal |
|---|---|---|---|---|---|
| Place value | 8 | 4 | 2 | 1 | |
0b1100 (a) |
1 | 1 | 0 | 0 | 12 |
0b1010 (b) |
1 | 0 | 1 | 0 | 10 |
&^ (AND-NOT) |
0 | 1 | 0 | 0 | 4 |
Only bit position 2 survives, because that is the only place where a has a 1 and b has a 0. Everywhere b has a 1, the result is cleared.
&^ is used to clear specific bits. C programmers write a & ~b for the same effect; Go folds it into a single operator.
Left and right shift (<<, >>)
Shifting moves every bit by a number of positions. Left shift fills zeroes in from the right. Right shift moves bits the other way; for the positive values in this lesson, that also means zeroes come in from the left:
fmt.Println(1 << 3) // 8 (0b0001 shifted three places left is 0b1000)
fmt.Println(16 >> 2) // 4 (0b10000 shifted two places right is 0b00100)
Left-shifting by n multiplies by 2^n. Right-shifting by n divides by 2^n for positive values. Signed negative values are trickier: right shift keeps the sign bit, so -16 >> 2 is -4, not a large positive number. The compiler already recognises simple multiply-by-power-of-two cases, so you rarely use shifts for speed. Where they do appear is in binary data and flag manipulation.
Where bitwise actually shows up
You will rarely write bitwise code in ordinary application Go. The place you will see it is a set of independent on/off flags packed into one integer: Unix file permissions, network socket options, and other "bag of choices" values. Each flag is a power of two, which means its binary form has exactly one bit set. The iota shift idiom from the Constants lesson is how you declare them:
const (
FlagRead = 1 << iota // 0b001 = 1
FlagWrite // 0b010 = 2
FlagExecute // 0b100 = 4
)
With those constants in place, the three most common operations have concrete meanings:
flags := FlagRead | FlagWrite // combine, 0b011
hasWrite := flags&FlagWrite != 0 // test, true
flags = flags &^ FlagWrite // clear, back to 0b001
That pattern is how Go's own os.FileMode represents file permissions. Having a mental model for what &, |, ^, &^, <<, and >> do is enough to read code like that without getting lost, even if you rarely write it yourself.
The starter already prints the results of &, |, ^, and one combined flag value. Extend it with four more lines:
- Print
0b1100 &^ 0b1010. It should print4. - Print
1 << 3. It should print8. - Print
flags&FlagWrite != 0. It should printtrue. - Clear the write flag with
flags = flags &^ FlagWrite, then printflags. It should print1.
8 14 6 3 4 8 true 1