Shift Operators

Understand Go's left shift and right shift operators — how they manipulate bits, relate to multiplication and division by powers of two, and what pitfalls to avoid

Shift operators move the bits of an integer left or right. Go provides two: << for left shift and >> for right shift. Both take two operands — the value to shift and the non‑negative shift count. If you have worked with multiplication or division in decimal, think of shifting bits as the binary equivalent of sliding the decimal point: moving left multiplies by the base, moving right divides.

Left Shift (<<)

The expression x << n takes the bit pattern of x and moves every bit n positions to the left. The right side of the number is filled with 0 bits, and bits that move past the leftmost position are discarded.

For unsigned integers, left shifting by n is equivalent to multiplying by 2ⁿ. For signed integers the same arithmetic holds as long as the result does not overflow.

package main
import "fmt"
func main() {
    var a uint8 = 6   // binary: 00000110
    b := a << 1       // binary: 00001100 → decimal 12
    fmt.Println(b)    // 12
    c := a << 3       // 6 × 2³ = 48 → binary: 00110000
    fmt.Println(c)    // 48
}

The output 12 and 48 confirms that each left shift by one doubles the value. Because uint8 can hold values up to 255, shifting 6 three times still fits. Shift 6 by 6 positions and you get 6 × 64 = 384, which overflows an 8‑bit type — uint8 wraps modulo 256, so 6 << 6 gives 128. The compiler does not warn; the bits simply fall off the left end.

Left-shift overflow is silent:

Go does not raise an error or panic when a left shift overflows the type’s range. The result wraps according to two’s‑complement arithmetic. Always check that the maximum value your type can hold is larger than x × 2ⁿ.

Beginners often think the lost bits "wrap around" to the right side. A left shift is not a rotation. Bits pushed past the left boundary are gone — they do not reappear on the right.

Right Shift (>>)

The expression x >> n moves every bit n positions to the right. The new bits added on the left depend on whether x is a signed or unsigned integer. Go uses this distinction directly: there is no separate unsigned‑right‑shift operator.

  • Unsigned integers receive 0 bits on the left — a logical shift.
  • Signed integers receive copies of the original sign bit (the leftmost bit) — an arithmetic shift. This preserves the sign.
package main
import "fmt"
func main() {
    var u uint8 = 12   // binary: 00001100
    fmt.Println(u >> 1) // 6  (00000110)
    fmt.Println(u >> 2) // 3  (00000011)
    var s int8 = -12   // binary (two's complement): 11110100
    fmt.Println(s >> 1) // -6  (11111010)
    fmt.Println(s >> 2) // -3  (11111101)
}

For unsigned 12, shifting right once divides by 2 and yields 6. For signed -12, each shift still divides the magnitude by 2 but keeps the number negative because the leftmost 1 bits are replicated. The result of -12 >> 1 is -6, not 6.

Arithmetic shift does not equal Go’s integer division for negatives:

-5 >> 1 gives -3 (the bits 1111101111111101). In contrast, -5 / 2 in Go yields -2 because integer division truncates toward zero. Do not assume x >> n is always the same as x / (1 << n) when x is negative.

When a right shift pushes bits past the right end, those bits are discarded. You cannot recover the original value by shifting back left — the information is gone.

How Go Chooses Between Logical and Arithmetic Shift

There is only one >> operator. The hardware‑level operation it maps to is determined at compile time by the type of the left operand.

Left operand typeRight shift behaviourEquivalent C operation
uint, uint8, …logical (fills with 0)unsigned >>
int, int8, …arithmetic (replicates sign bit)Implementation‑defined in C

This design is deliberate: it spares the programmer from having to remember a second operator while making signed‑shift behaviour predictable. In C, what >> does on a signed int is left to the implementation; in Go, it is always arithmetic and guaranteed.

No guesswork with signed right shift:

Any Go compiler must replicate the sign bit for signed integers. If you need a logical right shift on data that happens to be stored in a signed variable, cast it to an unsigned type first: uint(x) >> n.

Shift Count Rules and Undefined Behaviour

The right operand — the shift count — must be a non‑negative integer. Go enforces this strictly, but the consequences differ between compile‑time and run‑time.

  • Constant shift count: If the count is negative, the compiler refuses the program with an error. If the count is larger than or equal to the bit width of the left operand’s type, the behaviour is implementation‑defined — it may produce 0, -1, or anything else, and should not be relied upon.
  • Non‑constant shift count: If the count is negative at run time, the program panics. If the count is too large (≥ bit width), the behaviour is also implementation‑defined. Therefore, always validate or constrain shift counts that come from user input or calculations.
package main
import (
    "fmt"
    "os"
)
func main() {
    x := 1
    shift := -1
    // Uncommenting the next line would cause a run-time panic:
    // fmt.Println(x << shift)
    shift = 64 // on a 64-bit int, this is too large
    // The result is implementation-defined; do not depend on it.
    fmt.Println(x << shift)
    // Best practice: guard the shift.
    if shift < 0 || shift >= 64 {
        fmt.Fprintln(os.Stderr, "shift count out of range")
        return
    }
    fmt.Println(x << shift)
}

Overshifting is not safe:

Shifting a 64‑bit integer by 64 or more positions falls into implementation‑defined territory. Even if one compiler appears to give 0, another version or target architecture may behave differently. Treat shift counts above the type’s bit width as a bug.

Mental Model for Beginners

If you imagine a row of light switches that are either on (1) or off (0), shifting left moves each switch’s position one step to the left. The rightmost switch gets turned off, and the switch that falls off the left end breaks off and disappears. Shifting right is the same, but the new switch that appears on the left copies the state of the previous leftmost switch if the row represents a signed number; otherwise it stays off.

That model explains the core rules:

  • Left shift always fills with 0.
  • Right shift fills with 0 for unsigned types, and with the sign bit for signed types.
  • Bits that are pushed out are gone forever.

A concrete decimal analogy: “multiply by 10” shifts digits left and appends a zero. “Integer divide by 10” shifts digits right and discards the rightmost digit. For binary, multiplying by 2 is a left shift, and integer division by 2 is a right shift — with the sign caveat.

Common Use Cases

Shift operators appear frequently in systems programming, graphics, networking, and anywhere bits are used to pack information.

Fast Multiplication and Division by Powers of Two

For performance‑critical code, x << n and x >> n are cheaper than general multiplication or division — though modern compilers already replace * 8 with a shift when they can. The explicit form is still valuable when the factor is itself computed dynamically.

// Compute 2ⁿ as a bit mask
mask := 1 << n

Creating Bit Masks and Extracting Bit Fields

A left shift builds a mask with a single 1 at position n. Combine with & to test or clear that bit.

const (
    flagRead  = 1 << iota // 1
    flagWrite             // 2
    flagExec              // 4
)
func hasPermission(flags, perm byte) bool {
    return flags&perm != 0
}

A right shift moves a bit field down to the least‑significant position so you can interpret it as a small integer.

// Extract 3-bit field starting at bit 4
field := (value >> 4) & 0b111

Circular Shifts (Bit Rotation)

Go has no built‑in rotate operator, but you can compose one with two shifts and a bitwise OR. The following works for unsigned integers when you know the bit width.

func rotateLeft8(x uint8, n int) uint8 {
    n %= 8
    return (x << n) | (x >> (8 - n))
}

The left‑shifted bits fill the lower part; the right‑shifted bits wrap the upper part around to the bottom. For signed types the right shift would replicate the sign bit, so always use unsigned types when implementing rotation.

Pitfalls to Watch For

Most mistakes fall into a small set of patterns. Knowing them ahead of time prevents subtle bugs.

  • Assuming shifts are rotations. Bits that fall off one end are lost. If you need circular behaviour, write it explicitly.
  • Treating >> on signed integers as division. As shown earlier, -5 >> 1 is -3, while -5 / 2 is -2. Use shifts for bit manipulation, not as a drop‑in replacement for signed division.
  • Shifting by the full width or more. 1 << 64 (on a 64‑bit int) is implementation‑defined, not 0. Always keep the shift count under the bit width.
  • Negative shift counts. A variable shift that becomes negative panics at run time. Always sanitise input or ensure it is unsigned.
  • Ignoring type promotion. When the left operand is an untyped constant and the right operand is a typed variable, the constant is converted to the variable’s type before shifting — which can change the result width.

Performance is not the primary reason to use shifts:

In most Go programs, the compiler’s own optimisations handle multiplication and division by powers of two perfectly well. The real value of shift operators is in bit‑packing, protocol parsing, and low‑level hardware interfaces where you need to construct or dismantle bit patterns directly.

Summary

Go’s << and >> operators give you direct control over the position of every bit in an integer. Left shift multiplies by a power of two; right shift divides — but with signed integers it performs an arithmetic shift that preserves the sign, which does not always match Go’s truncating division. The shift count must be non‑negative and smaller than the bit width of the type; anything else either panics or enters implementation‑defined territory.

When you need to pack flags into a single byte, extract a bit field from a hardware register, or write a tight inner loop that multiplies by a power of two, shifts are the right tool. When you simply need to divide two numbers, use the division operator — it is clearer and never surprises you with sign‑extension behaviour.