How to Use the Shift Operators << and >> in Go

Use the `<<` operator to shift bits left (multiplying by powers of 2) and `>>` to shift bits right (dividing by powers of 2), keeping in mind that Go shifts are always unsigned for the count and preserve the sign bit for signed integers during right shifts.

When bits need to move

You are parsing a binary protocol. The specification says the first four bytes contain a packed header: the top 8 bits hold the version, the next 4 bits hold flags, and the bottom 12 bits hold a sequence number. You read the raw bytes into a uint32. Now you need to extract the flags without touching the version or the sequence number. Division and modulo could work, but they obscure the intent. Bit shifts are the scalpel here. They slide bits around so you can isolate exactly the field you need, then mask off the rest.

Shift operators also appear when you manage permissions, pack data into fixed-width structures, or implement low-level algorithms. Go's shift operators are simple, but they have strict rules about types and signs that prevent silent bugs. Understanding those rules makes the operators reliable tools rather than sources of confusion.

How shifts work

An integer is a sequence of bits. Each bit represents a power of two. A shift operator moves every bit left or right by a specified number of positions. Bits that fall off the edge are discarded. New bits fill in on the opposite side.

Left shift (<<) moves bits toward higher significance. New bits on the right are always zero. The value multiplies by two for each position shifted. Right shift (>>) moves bits toward lower significance. New bits on the left depend on the type. For unsigned integers, the new bits are zero. For signed integers, the new bits copy the sign bit, preserving whether the number is positive or negative.

The shift count is always treated as an unsigned integer. Go enforces this at compile time. If you try to shift by a negative value, the compiler rejects the program. The shift count also cannot exceed the width of the type in a meaningful way. If the shift count is greater than or equal to the bit width, the result is zero. Go does not wrap the shift count; it simply produces zero.

Minimal example

Here is the basic mechanics: shift left multiplies, shift right divides, and the binary representation moves accordingly.

package main

import "fmt"

func main() {
    // Start with 1, which is 0b00000001 in binary
    base := uint8(1)

    // Shift left by 3 positions: 0b00000001 becomes 0b00001000 (8)
    // Each left shift multiplies the value by 2
    doubled := base << 3
    fmt.Printf("1 << 3 = %d\n", doubled)

    // Shift right by 1 position: 0b00001000 becomes 0b00000100 (4)
    // Each right shift divides by 2, discarding the remainder
    halved := doubled >> 1
    fmt.Printf("8 >> 1 = %d\n", halved)

    // Shift count must be unsigned; negative counts cause a compile error
    // The compiler rejects this with: invalid operation: shift count type int must be unsigned integer or untyped integer
    // badShift := base << -1
}

Shifts move bits. They do not change the type. Cast explicitly if the semantics change.

What happens under the hood

When you write x << n, the compiler takes the binary representation of x and moves every bit n positions to the left. Bits that exceed the type width vanish. Zeros fill in on the right. The value grows exponentially. x >> n moves bits right. Bits fall off the right edge. For unsigned types, zeros fill on the left. For signed types, the sign bit replicates on the left.

Consider a signed int8. The value -1 is represented as all ones in two's complement: 0b11111111. Shifting right by one position yields 0b11111111, which is still -1. The sign bit is one, so the new bit is one. The value stays negative. This is an arithmetic shift. It preserves the sign. If you expect a logical shift that fills with zeros, you must cast the value to an unsigned type first.

The shift count n is evaluated as an unsigned integer. Go requires the shift count to be of unsigned integer type or an untyped constant representable as uint. If you pass a variable of type int as the shift count, the compiler complains with invalid operation: shift count type int must be unsigned integer or untyped integer. This rule eliminates ambiguity. A negative shift count has no sensible meaning, so Go forbids it entirely.

Realistic example: bit flags

Here is how you manage flags in production code using iota and bitwise operations.

package main

import "fmt"

// Define permissions using iota to generate unique bit positions
// iota resets to 0 at each const block, so these become 1, 2, 4, 8
// Go convention: use iota for bit flags to keep values distinct and auto-incrementing
const (
    PermRead  = 1 << iota // 0b0001
    PermWrite             // 0b0010
    PermExecute           // 0b0100
    PermDelete            // 0b1000
)

func main() {
    // Combine flags using bitwise OR to set multiple bits at once
    // 0b0001 | 0b0100 results in 0b0101 (5)
    userPerms := PermRead | PermExecute

    // Check a flag by ANDing with the mask and comparing to zero
    // If the bit is set, the result is non-zero
    canRead := (userPerms & PermRead) != 0
    fmt.Printf("Can read: %v\n", canRead)

    // Toggle a flag using XOR
    // XOR flips the bit: 1 becomes 0, 0 becomes 1
    userPerms ^= PermExecute
    canExec := (userPerms & PermExecute) != 0
    fmt.Printf("Can execute after toggle: %v\n", canExec)

    // Clear a flag using AND with the complement
    // &^ is the bitwise AND NOT operator in Go
    userPerms &^= PermRead
    canReadAgain := (userPerms & PermRead) != 0
    fmt.Printf("Can read after clear: %v\n", canReadAgain)
}

Go developers use iota for bit flags. It generates sequential values starting from zero. Combine it with 1 << iota to get powers of two. gofmt aligns the block, so you write the shift only once on the first line. The formatter handles the rest. This convention keeps flag definitions compact and error-free.

Shifts move bits. They don't change the type. Cast explicitly if the semantics change.

Pitfalls and compiler errors

The most common mistake is assuming right shift behaves the same for signed and unsigned integers. Go's right shift on signed integers is arithmetic. It preserves the sign. If you need a logical shift that fills with zeros, you must cast to unsigned.

package main

import "fmt"

func main() {
    // Signed right shift preserves the sign bit
    // -1 is all 1s in two's complement; shifting right fills with 1s
    signedVal := int8(-1)
    arithmeticShift := signedVal >> 1
    fmt.Printf("-1 >> 1 = %d\n", arithmeticShift) // prints -1

    // Cast to unsigned for a logical shift
    // uint8(-1) is 255 (0b11111111); shifting right fills with 0
    logicalShift := uint8(signedVal) >> 1
    fmt.Printf("uint8(-1) >> 1 = %d\n", logicalShift) // prints 127
}

Another pitfall is shifting by an amount that exceeds the type width. Go does not wrap the shift count. If the shift count is greater than or equal to the bit width, the result is zero. This prevents silent bugs where a shift wraps around and produces garbage.

package main

import "fmt"

func main() {
    val := uint8(1)

    // Shifting by exactly the width yields zero
    fmt.Println(val << 8) // 0

    // Shifting by more than the width also yields zero
    fmt.Println(val << 16) // 0

    // The shift count is masked? No. Go spec says result is zero.
    // This is safe. It avoids undefined behavior.
}

The compiler enforces the shift count type strictly. If you use a signed integer variable as the shift count, the compiler rejects the program with invalid operation: shift count type int must be unsigned integer or untyped integer. This error saves you from runtime surprises. Trust the type system. Shift counts must be unsigned.

When to use shifts

Use a left shift when you need to multiply by a power of two or set a specific bit position in a mask. Use a right shift on unsigned integers when you need to divide by a power of two or extract a field from a packed binary value. Use a right shift on signed integers only when you want to preserve the sign of the number during division. Use a cast to unsigned before shifting right when you need a logical shift that fills with zeros instead of copying the sign bit. Use bitwise OR and AND with shift constants when you are managing a set of boolean flags in a single integer. Use plain multiplication and division when the code is more readable and the compiler optimizes the operation anyway.

The compiler optimizes arithmetic. Shift for bits, not for speed.

Where to go next