Operator Precedence

How Go groups operators in expressions, the precedence table, associativity rules, and common pitfalls when mixing different operators.

Operator precedence is the set of rules that decides which operator gets applied first when an expression has more than one operator. It is the same idea as the arithmetic rules you learned in school: in 1 + 2 * 3, multiplication happens before addition because multiplication has higher precedence. Without precedence rules, an expression like 1 + 2 * 3 would be ambiguous — and every expression with more than one operator would need parentheses.

Go defines a strict, two-part precedence system: unary operators always have the highest precedence, and binary operators are split into five levels. The table below shows the full order, from highest to lowest priority.

Go’s Precedence Table

PrecedenceOperatorsNotes
HighestUnary operators+ - ! ^ * & <-
5* / % << >> & &^Multiplication, division, remainder, shifts, bitwise AND, bit clear
4+ - | ^Addition, subtraction, bitwise OR, bitwise XOR
3== != < <= > >=Comparison operators
2&&Logical AND
1||Logical OR

Binary operators on the same row have equal precedence. When several operators with the same precedence appear in a row, associativity decides the order — and all Go binary operators associate left to right.

Unary operators are right-associative. The expression *p++ is parsed as (*p)++, not as *(p++). We will return to this detail shortly.

No Assignment Expressions:

Go does not treat assignment as an operator that produces a value. Statements like x = y = 5 are illegal, so assignment precedence is not part of this table.

Associativity: Left-to-Right Grouping

When an expression contains multiple operators at the same precedence level, the compiler groups the operands starting from the left. For example, x / y * z is the same as (x / y) * z — the division happens first because the leftmost / of the pair / and * binds first.

a, b, c := 24, 4, 2
result := a / b * c   // (24 / 4) * 2 = 6 * 2 = 12

If associativity were right-to-left, the result would be 24 / (4 * 2) = 3. This left-to-right rule applies uniformly to all binary operators in Go.

This means an expression like a == b == c is parsed as (a == b) == c. Since a == b produces a boolean, the compiler then attempts to compare that boolean to c with ==, which usually fails because c is not a boolean. Go will report a type error rather than silently produce a surprise.

x, y, z := 1, 1, 2
// The following line will not compile:
// fmt.Println(x == y == z)
// Type mismatch: bool == int

Unary Operators and Increment/Decrement

Unary operators — +, -, !, ^, *, &, <- — sit at the top of the hierarchy. They bind right-to-left, which means that when you stack them, the one on the right applies first.

x := 10
p := &x
fmt.Println(*p)    // 10
fmt.Println(^x)    // bitwise complement of 10

Go’s increment and decrement operators, ++ and --, are statements, not expressions. They do not produce a value and therefore sit completely outside the precedence table. A common misconception when coming from C is to write *p++ and expect it to dereference p and then advance the pointer. In Go, *p++ is parsed as (*p)++ — the dereference happens first, and the ++ applies to the integer *p, not the pointer p. The pointer p is never altered.

arr := [3]int{10, 20, 30}
p := &arr[0]
// Increment the integer at *p, p stays pointing at arr[0]
*p++
fmt.Println(*p, arr[0]) // 11 11

++ and -- Do Not Return a Value:

Because ++ and -- are statements, you cannot write x = y++ or use ++ inside a larger expression. The code x = y++ is a compile-time error.

How Go’s Precedence Differs From C

If you are coming from C, C++, Java, or similar languages, the general shape of the table will feel familiar: multiplicative before additive, additive before comparison, comparison before logical AND before logical OR. But there are differences that can lead to real bugs when porting code or writing in both languages. The most impactful difference concerns shift operators.

In C, + has higher precedence than <<. Therefore x << 1 + 2 means x << (1 + 2). In Go, << has the same precedence as * — higher than + — so the exact same expression x << 1 + 2 means (x << 1) + 2.

// Go
x := 1
result := x<<1 + 2   // (1 << 1) + 2 = 2 + 2 = 4
/* C */
int x = 1;
int result = x << 1 + 2; // x << (1 + 2) = 1 << 3 = 8

Shift Precedence Is Different From C:

If you transliterate C code that uses shifts without adding parentheses, you will get different results in Go. This is the most common precedence surprise for developers with a C background.

Another difference: Go has no ternary conditional operator (?:) and no assignment expressions, so those precedence levels simply do not exist. The bit-clear operator &^ is unique to Go and lives at the same level as &.

Common Mistakes and How to Avoid Them

Operator precedence mistakes are rarely dramatic explosions. They produce wrong numeric results, skewed conditionals, or compile-time type errors that seem confusing until you check the precedence table.

Logical Operators Without Parentheses

The logical AND && has higher precedence than ||. This matches boolean algebra and is exactly what most programmers expect.

// Parsed as: a || (b && c)
if user.IsAdmin || user.IsActive && user.HasPaid {
    // ...
}

However, long conditionals that mix the two without parentheses quickly become unreadable. Even when the precedence is correct, explicit parentheses make the intent obvious.

if user.IsAdmin || (user.IsActive && user.HasPaid) {
    // The grouping is visible at a glance
}

Mix of && and || Hides Intent:

Relying solely on precedence for complex boolean expressions often leads to logic errors during maintenance. Use parentheses even when they are technically unnecessary.

Bitwise Operators and Comparison

Go places bitwise AND (&) and bit clear (&^) at the same level as multiplication — well above comparison. So flags & mask == 0 is parsed as (flags & mask) == 0, which is usually what you want. But if you come from a language where == binds tighter than &, you might add unnecessary parentheses or, worse, mis-read existing code.

const Read = 1 << iota
const Write
mask := Read | Write
// Checks if no permission bits are set
if mask&Read == 0 {       // (mask & Read) == 0
    fmt.Println("Read not set")
}

The real danger is the bitwise OR | and XOR ^ at level 4: they bind tighter than == (level 3). So a | b == 0 is a | (b == 0), not (a | b) == 0. This is usually a bug.

a := 2
b := 0
// a | b == 0  →  a | (b == 0)  →  2 | true
// The compiler rejects this: mismatched types

Bitwise OR vs. == Can Cause Type Errors:

In Go, a | (b == 0) tries to apply bitwise OR to an integer and a boolean, producing a compilation error. If you meant to test whether the bitwise result is zero, you must write (a | b) == 0.

String Concatenation and Arithmetic

The + operator does double duty: addition for numbers and concatenation for strings. Both uses have the same precedence (level 4) and are left-associative. So mixing numbers and strings in the same expression can yield surprising compile-time errors or unintended results.

// "1 + 2 = " + 1 + 2   →   "1 + 2 = 1" + 2  →  "1 + 2 = 12"
// The first + is concatenation because "1 + 2 = " is a string.
// After that, the remaining + still sees a string left operand, so continues concatenating.
fmt.Println("1 + 2 = " + 1 + 2)   // prints: 1 + 2 = 12

The parentheses change the order: "1 + 2 = " + (1 + 2) first adds the integers, then concatenates.

fmt.Println("1 + 2 = " + (1 + 2)) // prints: 1 + 2 = 3

Chained Comparisons Are Illegal

You cannot write a < b < c in Go. The parser treats it as (a < b) < c, which compares a boolean to c — a type error. If you need to check that a value lies within a range, use separate comparisons combined with &&.

if 0 <= x && x < 10 {
    fmt.Println("x is in [0, 10)")
}

Compiler Catches Chained Comparisons:

Go’s type system prevents accidental chained comparisons. The expression a < b < c always causes a compile-time error, never a silent logic bug.

Using Parentheses to Control Evaluation

Parentheses override the default precedence and associativity rules. Everything inside parentheses is evaluated as a single unit before the surrounding operators are applied.

// (1 + 2) * 3 = 9, not 7
result := (1 + 2) * 3

They are not just a debugging tool; they are a legitimate part of writing clear, maintainable Go code. Even when the precedence table makes a grouping obvious to the compiler, it may not be obvious to the next developer. Adding parentheses costs nothing at runtime — the Go compiler evaluates constant subexpressions at compile time and produces the same machine code with or without them — and it can prevent mistakes.

const secondsPerDay = 60 * 60 * 24
// This line is technically unambiguous but harder to scan:
const weekSeconds = 60 * 60 * 24 * 7
// This reads in one glance:
const weekSecondsClear = (60 * 60 * 24) * 7

Parentheses Do Not Affect Performance:

Parentheses are resolved entirely during compilation. They add no runtime overhead and never hurt performance.

Summary

Operator precedence in Go is deliberately simple: unary operators at the top, five levels of binary operators, left-to-right associativity for all of them. The table is small enough to remember, and the compiler’s type checking catches many of the ambiguous constructs that other languages let through silently — like chained comparisons or bitwise OR applied to a boolean.

The one precedence rule that causes the most porting bugs from C is the shift operators: << and >> bind tighter than + in Go, the reverse of C. When you move C arithmetic that mixes shifts and additions into Go, always add parentheses to preserve the original meaning, even if just to make the intent readable in the new context.