Go Operator Precedence Table

Go operator precedence determines the order of evaluation for expressions, with multiplication and shifts binding tighter than addition and comparisons.

When the compiler reads your expression differently than you do

You write a quick check to verify a permission flag. You type if mask & Admin == Admin { ... }. The code compiles. It runs. The logic works perfectly. You port that same line to a C project. It breaks. You paste it into a Python script. It breaks. You didn't write bad code. You wrote Go code that relies on Go's specific rules for how operators bind together.

Operator precedence is the invisible grammar of your expressions. It decides which symbols grab their neighbors first. Without precedence, an expression like a + b * c is ambiguous. Does it mean (a + b) * c or a + (b * c)? Go resolves this ambiguity by assigning a binding strength to every operator. Some operators act like super glue, pulling their operands together before weaker operators get a chance. Others act like loose tape.

Get precedence wrong, and the compiler might accept your code while the logic silently computes the wrong value. Or the compiler rejects it with a type mismatch that makes no sense until you see how the compiler grouped the terms. Understanding precedence isn't about memorizing a table. It's about knowing how Go parses your intent so you can write code that matches your brain, not just the spec.

The hierarchy of binding

Go's precedence rules follow mathematical tradition with deliberate deviations to prevent common bugs. The spec defines levels from highest binding strength to lowest. Operators at the same level associate left to right, except for unary operators which associate right to left.

The hierarchy breaks down into five main tiers:

  1. Unary operators: + - ! ^. These apply to a single operand. !x or ^x or -x. They bind tightest.
  2. Multiplicative and bitwise shifts: * / % << >> & &^. Multiplication, division, modulo, left shift, right shift, bitwise AND, and bitwise clear.
  3. Additive and bitwise OR/XOR: + - | ^. Addition, subtraction, bitwise OR, and bitwise XOR.
  4. Comparisons: == != < <= > >=. Equality and ordering checks.
  5. Logical operators: && then ||. Logical AND has higher precedence than logical OR.

This structure means * binds tighter than +, just like in math. It also means << binds tighter than +, which makes sense because a left shift is effectively multiplication by a power of two. The surprising part for developers coming from C or Python is the relationship between bitwise operators and comparisons. In Go, bitwise operators like & bind tighter than ==. This is a deliberate design choice that saves you from a whole class of bugs.

Minimal example: arithmetic and grouping

The most common interaction is between multiplication and addition. Multiplication sits at a higher precedence level, so it grabs its operands first.

package main

import "fmt"

// Main demonstrates basic arithmetic precedence.
func main() {
	// Multiplication binds tighter than addition.
	// The compiler groups 2 * 3 first, then adds 10.
	// Result is 10 + 6 = 16.
	result := 10 + 2 * 3
	fmt.Println(result)

	// Parentheses override precedence.
	// You force the addition to evaluate before multiplication.
	// Result is 12 * 3 = 36.
	result = (10 + 2) * 3
	fmt.Println(result)
}

When the compiler sees 10 + 2 * 3, it scans for the highest precedence operator. It finds *. It evaluates 2 * 3 to get 6. The expression becomes 10 + 6. It evaluates the addition to get 16. Parentheses create a sub-expression. The compiler evaluates everything inside the parentheses first, regardless of precedence. You use parentheses to make your intent explicit or to force a different evaluation order.

Parentheses are cheap. Ambiguity is expensive.

The Go difference: bitwise operators and comparisons

Developers coming from C, C++, Java, or Python often trip over precedence because those languages give comparisons higher precedence than bitwise operators. In C, mask & flag == flag parses as mask & (flag == flag). The equality check happens first, producing a boolean, which then gets bitwise-ANDed with the mask. This usually causes a type error or a logic bug.

Go flips this. Bitwise & has higher precedence than ==. The expression mask & flag == flag parses as (mask & flag) == flag. This matches the intuition of anyone checking a bitmask. You mask the value, then you compare the result. Go's precedence prevents the C-style trap automatically.

This also applies to | and ^. They bind tighter than comparisons. You can write status | Error == status and Go groups it as (status | Error) == status. You don't need parentheses for simple bitmask checks in Go. You do need them if you mix levels in complex ways, but the common case is safe.

Go saves you from C traps, but not from your own assumptions.

Realistic example: the shift trap

While Go protects you from the bitmask comparison bug, it doesn't protect you from mixing shifts with addition. Shifts bind tighter than addition. This causes subtle bugs in address calculations or buffer offsets where you expect addition to happen before the shift.

package main

import "fmt"

// CalculateOffset demonstrates a precedence trap with shifts.
func CalculateOffset(base int, count int, shift int) int {
	// WRONG: Shift binds tighter than addition.
	// This evaluates as base + (count << shift).
	// If you wanted (base + count) << shift, this is broken.
	return base + count << shift
}

// CalculateOffsetFixed shows the correct grouping.
func CalculateOffsetFixed(base int, count int, shift int) int {
	// Parentheses force addition before the shift.
	// The sum is shifted as a single unit.
	return (base + count) << shift
}

func main() {
	// base = 10, count = 2, shift = 3.
	// 10 + 2 << 3 -> 10 + (2 << 3) -> 10 + 16 = 26.
	fmt.Println(CalculateOffset(10, 2, 3))

	// (10 + 2) << 3 -> 12 << 3 -> 96.
	fmt.Println(CalculateOffsetFixed(10, 2, 3))
}

If you are computing a memory address or a byte offset, base + count << shift likely computes the wrong value. You probably want to add the base and count, then shift the total. The compiler accepts the code because the types are valid. The error is purely logical. The compiler complains with invalid operation: operator not defined on type only if the grouping produces a type mismatch, like trying to shift a boolean. Otherwise, it trusts your syntax and runs the wrong math.

Always parenthesize shifts when they mix with addition or subtraction. The visual grouping matches the logical grouping.

Short-circuiting: logical vs bitwise

Go distinguishes between logical operators && and || and bitwise operators & and |. This distinction matters for evaluation order and side effects.

Logical operators short-circuit. && evaluates the left operand. If it is false, the right operand never evaluates because the result is already false. || evaluates the left operand. If it is true, the right operand never evaluates. This is essential for guard clauses.

Bitwise operators do not short-circuit. Both operands always evaluate. Using & instead of && in a conditional can cause panics or wasted work.

package main

import "fmt"

// CheckPointer demonstrates short-circuiting safety.
func CheckPointer(ptr *int) bool {
	// && short-circuits.
	// If ptr is nil, the dereference never happens.
	// This is safe.
	if ptr != nil && *ptr > 0 {
		return true
	}
	return false
}

// CheckPointerUnsafe shows the danger of bitwise operators.
func CheckPointerUnsafe(ptr *int) bool {
	// & does not short-circuit.
	// Both sides always evaluate.
	// *ptr dereferences nil, causing a panic.
	// The compiler accepts this because & works on booleans.
	if ptr != nil & *ptr > 0 {
		return true
	}
	return false
}

func main() {
	var p *int
	fmt.Println(CheckPointer(p))      // false. Safe.
	// fmt.Println(CheckPointerUnsafe(p)) // panic: runtime error.
}

The compiler accepts ptr != nil & *ptr > 0 because & is defined for boolean types. It produces a boolean result. The syntax is valid. The runtime behavior is a panic. This is a classic pitfall. Use && and || for control flow. Use & and | only for bitwise manipulation of integers or when you explicitly need non-short-circuiting boolean evaluation (which is rare).

Short-circuiting is a safety net. Bitwise operators are raw logic. Pick the right tool.

Pitfalls and compiler errors

Precedence errors often manifest as type mismatches or unexpected values. The compiler helps when the grouping produces incompatible types.

If you write x & y == z where x, y, and z are integers, Go parses it as (x & y) == z. This is usually what you want. If you accidentally write x == y & z, Go parses it as x == (y & z). This is also valid. The compiler only rejects the code if the grouping creates a type error.

For example, if you mix a boolean and an integer without parentheses, the compiler rejects the program with invalid operation: operator not defined on type. If you write mask & flag == true, Go parses it as (mask & flag) == true. This is valid, but comparing an integer result to true is redundant and confusing. The compiler accepts it, but linters will flag it.

Another pitfall is the XOR operator ^. In Go, ^ is bitwise XOR, not exponentiation. It has the same precedence as |. If you expect mathematical power, you need math.Pow. The compiler won't stop you from writing 2 ^ 10, but it will compute 2 XOR 10, which is 8, not 1024.

Unary operators also have quirks. ^x is bitwise NOT for integers. !x is logical NOT for booleans. They have the highest precedence. !x & y parses as (!x) & y. If x is a boolean and y is an integer, this fails with a type error.

Convention aside: the community accepts if err != nil as the standard guard. Never use & or | in error checks. The verbosity of && is a feature. It signals control flow. It signals short-circuiting. It signals safety.

Trust the short-circuit. Write &&.

Decision: when to use parentheses and operators

Use parentheses when the expression mixes operators of different precedence levels and the grouping isn't immediately obvious to a reader scanning the code. Use parentheses when you are porting logic from C, Python, or JavaScript where precedence rules differ, to make the Go grouping explicit. Use default precedence when the expression uses only operators from the same level, such as a chain of additions or a chain of bitwise ANDs. Use bitwise operators &, |, ^, &^ for low-level flag manipulation and integer masking, relying on Go's higher precedence to keep comparisons safe. Use logical operators && and || for boolean control flow and guard clauses, taking advantage of short-circuiting to prevent panics and unnecessary work. Use shifts << and >> for multiplication or division by powers of two, but always parenthesize them when combined with addition or subtraction.

Parentheses cost nothing. Debugging a precedence bug costs time. Group explicitly.

Where to go next