Order of Evaluation
How Go determines the sequence in which expressions are evaluated, including left-to-right guarantees, short-circuit behavior, and assignment evaluation
Most languages leave the order in which subexpressions are computed deliberately unspecified, so compilers can reorder operations for optimization. Go takes the opposite approach: the evaluation order of expressions is defined precisely by the language specification. This means you can predict exactly when a function call runs, when a variable is read, or when a side effect takes place inside a complex expression.
That guarantee is a deliberate design choice. Unspecified evaluation order leads to code that behaves differently across compilers or platforms, and to bugs that are almost impossible to reproduce consistently. By locking down the sequence, Go removes an entire class of subtle, non-deterministic errors. The trade-off is that the compiler loses some optimization freedom, but the Go team decided that correctness and predictability matter more.
Evaluation Order vs. Operator Precedence:
Operator precedence determines how an expression is grouped — which operands belong to which operator. Evaluation order determines the sequence in which those grouped operands are actually computed at runtime. The two are separate concepts, and confusing them is one of the most common mistakes developers make when reading or writing expressions.
The Default Left-to-Right Rule
For any expression that is not a short-circuit logical operator or an assignment, Go evaluates the operands in strict left-to-right order. When you write f() + g(), the call to f always happens before the call to g. When you write a[i] + b[j], the index expressions i and j are evaluated left-to-right before any addition takes place.
This rule covers every operand in the expression tree, not just the outermost ones. Consider an expression with multiple levels of nesting:
x := f(g(), h()) + i(j(), k())
The evaluation sequence is:
g()(left operand off)h()(right operand off)f(g(), h())is called with the results of steps 1 and 2j()(left operand ofi)k()(right operand ofi)i(j(), k())is called with the results of steps 4 and 5- The addition of the two return values
Every function argument, index expression, and operand is visited from left to right, depth-first. You can rely on this exact sequence — the compiler will not reorder steps 1 and 2, nor will it call j() before f() has returned.
Predictable Side Effects:
If your functions have side effects (like logging, mutating a shared variable, or writing to a channel), the left-to-right guarantee means you can write code where the order of those side effects is explicit and reproducible. In languages with unspecified evaluation order, the same code might behave differently on a different compiler or even between builds.
Why This Matters in Practice
When the operands are pure functions with no side effects, the evaluation order is invisible — you get the same mathematical result regardless. The moment side effects enter the picture, order becomes observable. A common scenario is reading from a file or a network connection while simultaneously updating a counter:
var counter int
func readNext() int {
counter++
return 42 // some actual data
}
func main() {
result := readNext() + readNext()
fmt.Println(result, counter) // 84 2
}
Because readNext() is called left-to-right, the first call increments counter to 1, then the second call increments it to 2. The output is deterministic. In a language where evaluation order is unspecified, counter could be 1 or 2 depending on how the compiler chooses to sequence the calls — and that decision could change with optimization flags or compiler versions.
Short-Circuit Logical Operators
The logical AND (&&) and logical OR (||) operators are the one place where Go deliberately breaks the simple left-to-right rule. These operators evaluate their left operand first, and then decide whether to evaluate the right operand at all.
The rule:
- For
a && b: ifaevaluates tofalse,bis never evaluated. The whole expression isfalseregardless. - For
a || b: ifaevaluates totrue,bis never evaluated. The whole expression istrueregardless.
This is called short-circuit evaluation, and it has nothing to do with optimization — it is a language-level guarantee, not a compiler trick. If b contains a function call, that call simply does not happen when the left operand makes the result inevitable.
func expensiveCheck() bool {
fmt.Println("expensive check ran")
// Imagine a database query or API call here
return true
}
func main() {
userIsAdmin := false
if userIsAdmin && expensiveCheck() {
fmt.Println("access granted")
} else {
fmt.Println("access denied")
}
}
The output is only "access denied". The string "expensive check ran" never appears because userIsAdmin is false, so the right operand of && is short-circuited.
This is the same behavior you find in C, Java, JavaScript, and most other languages. Go does not invent something new here — it inherits a well-established convention and documents it explicitly so there is no ambiguity.
The Precedence Trap
A classic point of confusion arises when && and || appear together. Operator precedence dictates that && binds tighter than ||, so an expression like a || b && c is parsed as a || (b && c). Many developers incorrectly assume that precedence also forces b && c to be evaluated first. It does not.
func a() bool {
fmt.Println("a")
return true
}
func b() bool {
fmt.Println("b")
return true
}
func c() bool {
fmt.Println("c")
return true
}
func main() {
result := a() || b() && c()
fmt.Println("result:", result)
}
The output is:
a
result: true
Even though && has higher precedence, the evaluation order is still left-to-right on the operands of ||. a() runs first, returns true, and the entire || short-circuits. b() and c() never execute. Precedence controls grouping — it tells you which operands belong together — but it does not override the left-to-right evaluation sequence of the outermost operator.
A Source of Real-World Bugs:
Assuming that precedence drives execution order is the single most frequent evaluation-order mistake in Go (and in C-derived languages generally). When you write expensiveCheck() || cheapCheck() && quickCheck(), the grouping is expensiveCheck() || (cheapCheck() && quickCheck()), but expensiveCheck() still runs first. If expensiveCheck() returns true, neither of the other two functions ever runs. Code that expects cheapCheck() to have already executed may silently malfunction.
Assignment Statements and Multiple-Value Contexts
Assignments in Go have their own evaluation order rules that build on the left-to-right principle. The specification states that when a tuple assignment like a, b = expr1, expr2 is executed, all of the expressions on the right-hand side are evaluated first, in left-to-right order, and only then are the results assigned to the left-hand side variables.
This has a non-obvious consequence: the right-hand side expressions see the old values of the variables being assigned to. The following swap idiom works precisely because of this rule:
a, b := 1, 2
a, b = b, a
// Now a == 2, b == 1
b and a on the right are both evaluated before any assignment happens. If the assignment were performed left-to-right as a = b; b = a, the result would be a == 2, b == 2. The atomic evaluation of the right-hand side prevents that.
The same principle applies when functions with side effects appear on the right:
i := 0
a, b := i+1, i
fmt.Println(a, b) // 1 0
The left expression i+1 is evaluated first, when i is still 0, producing 1. Then the right expression i is evaluated, also producing 0. Only then are the results 1 and 0 assigned to a and b. If i had been modified inside the right-hand expressions — for example, if they were function calls that mutate a shared variable — the order would still be left-to-right, and every expression would see the cumulative effect of all mutations that occurred in expressions to its left.
Readability vs. Cleverness:
While the evaluation order is guaranteed, packing multiple side-effecting calls into a single assignment line makes the code hard to read and review. The guarantee is there so you can reason about code, not so you can cram an entire state machine into one statement. Use multiple lines when the logic is non-trivial.
The Special Case of Map Index Assignments
For an assignment of the form a[x] = f(), the evaluation order is: first a and x are evaluated (left to right), then f() is called, then the assignment happens. This means the map variable and index are computed before the right-hand side runs, which is important if f() modifies the map or the index variable.
m := map[int]string{0: "zero"}
i := 0
m[i] = func() string {
i = 1
return "one"
}()
fmt.Println(m[0]) // "one"
The index i is read as 0 before the function literal runs, so the assignment targets m[0]. The function then changes i to 1 and returns "one", which is stored in m[0]. The outcome is deterministic and matches the left-to-right specification.
Function Calls and Channel Operations
Function calls follow the same left-to-right rule for argument evaluation. When you write fmt.Println(f(), g()), the call to f() completes before g() begins, regardless of how the compiler might want to optimize. This is especially relevant when the functions communicate via shared state or channels:
var counter int
func next() int {
counter++
return counter
}
fmt.Println(next(), next(), next()) // Prints: 1 2 3
Each call to next sees the counter incremented by the previous call. The output is guaranteed to be 1 2 3 in that exact order.
Channel operations also participate in the left-to-right sequencing. When an expression involves a send or receive, the channel expression and the value expression (for sends) are evaluated in order:
ch := make(chan int, 1)
val := 10
ch <- val // val is evaluated, then the send occurs
The evaluation of val (reading the variable) happens before the send begins. In a compound expression like ch <- f() + g(), the sequence is: evaluate ch, evaluate f(), evaluate g(), perform the addition, then send the result. The send itself is the last step.
Ordering in Select Statements
The select statement has its own ordering rule for the channel expressions in each case. When a select is entered, all channel operands (the channels themselves for receive cases, and the channel and value expressions for send cases) are evaluated exactly once, in source order, before any case is chosen to proceed:
select {
case ch1 <- f():
fmt.Println("sent to ch1")
case v := <-ch2:
fmt.Println("received", v)
}
Here, ch1 and f() for the first case are evaluated, then ch2 for the second case. Only after all these evaluations complete does the runtime decide which communication can proceed. If f() has side effects, those side effects will have occurred regardless of which case is ultimately selected. This ordering is another deliberate guarantee — it prevents surprises where a side effect in a non-chosen case mysteriously fails to happen, or happens after the select has already committed to a different case.
Precedence Does Not Dictate Evaluation Order
This point has surfaced throughout the document, and it deserves its own focused treatment because it is the most persistent source of confusion around expression evaluation.
Operator precedence answers the question: "In the expression a + b * c, do I add a to b and then multiply by c, or multiply b by c and then add a?" The answer is multiply first, because * has higher precedence than +. Precedence determines the shape of the abstract syntax tree — it tells the parser which operands belong to which operator.
Evaluation order answers a different question: "In the expression a + b * c, do I evaluate a first, or b first, or c first?" In Go, the answer is: a, then b, then c, then b * c is computed, then a + result. In C, the answer is: the compiler can choose any order. In neither language does precedence decide the sequence.
The same distinction applies to && and ||. Precedence groups a || b && c as a || (b && c), but the left-to-right rule for || still evaluates a first. Only if a is false does evaluation proceed into the grouped subexpression (b && c), where b is then evaluated first (because && is left-to-right as well). The parentheses that precedence implies are invisible structural brackets, not runtime scheduling instructions.
You're on Solid Ground If You Remember This:
A practical mental model: precedence draws the tree before the program runs. Evaluation order walks the tree at runtime, always choosing the leftmost unvisited leaf first. The two operations happen at completely different phases. If you find yourself debugging an expression where the side effects are happening in an order you didn't expect, check whether you've accidentally assumed that a high-precedence subexpression runs before a low-precedence one. It never does — the walk always starts from the left.
Summary
Go's specification of evaluation order is not a minor detail — it is a structural commitment that reflects the language's broader philosophy. Where other languages embrace compiler freedom and treat evaluation order as an implementation detail, Go ties the compiler's hands deliberately so that programmers can read a complex expression and know, without running it, exactly when every function call, index operation, and channel communication will occur.
The key takeaways are:
- Left-to-right, always. For any expression that is not a short-circuit operator or an assignment, the operands are evaluated from left to right. This includes function arguments, index expressions, and the operands of arithmetic, comparison, and bitwise operators.
- Short-circuit operators evaluate their left operand first and may skip the right.
&&and||are the exceptions, and their behavior is identical to what you'd expect from C, Java, or JavaScript. - Assignments evaluate all right-hand expressions before any assignment occurs. This makes swap idioms and multi-return processing safe and predictable.
- Precedence groups; evaluation walks. Confusing these two concepts is the single most common evaluation-order mistake, and Go's guarantees don't save you from it — you still need to internalize that a high-precedence subexpression is not an instruction to run that part first.
Understanding evaluation order gives you the confidence to write expressions with side effects that behave identically on every Go compiler, on every platform, in every build configuration. That's a rare property among mainstream languages, and it's worth leaning on.