Other Operators and Expression Rules
Learn about Go's address and pointer operators, the full operator precedence table, and the rules that govern the order of evaluation inside expressions.
Go’s operator story doesn’t end with arithmetic and comparisons. Two operators work directly with memory—the address operator & and the pointer dereference operator *—and the language has precise rules for how complex expressions are grouped (precedence) and in what sequence their pieces are evaluated (order of evaluation). This section covers those remaining operators and the expression rules that every Go developer encounters as soon as they combine multiple operations on a single line.
Address and Pointer Operators
A variable occupies space in memory. Go gives you a way to ask “where exactly is that space?” and “what value is sitting at this address?” through two operators: & and *. They are the bridge between ordinary values and pointers.
What the & operator does
The & (address-of) operator returns a pointer to its operand. The operand must be addressable—something that has a fixed location in memory. Variables, array elements, struct fields, and slice elements all qualify. Literals, constants, and the return values of functions do not.
package main
import "fmt"
func main() {
x := 42
p := &x // p is a *int — a pointer to an int
fmt.Println(p) // prints a memory address, e.g. 0xc0000140a8
fmt.Println(*p) // prints 42 (see next section)
}
Here p is a variable of type *int. The expression &x produces the memory address where the integer x lives. The actual address printed will be different on every run; what matters is that the operator gives you a reference you can pass around and later follow back to the original value.
Not everything is addressable:
You cannot take the address of a map index expression directly because map elements are not addressable. This is a frequent source of confusion:
m := map[string]int{"a": 1}
// p := &m["a"] // compile error: cannot take address of m["a"]
v := m["a"]
p := &v // OK: v is a variable
Similarly, function return values and numeric literals are not addressable. The compiler will reject &(f()) and &(10).
What the * operator does
The * (pointer dereference) operator does the opposite: given a pointer, it retrieves the value stored at that address. If p is a pointer to an int, then *p is the int itself.
x := 10
p := &x
fmt.Println(*p) // 10
*p = 20 // write through the pointer
fmt.Println(x) // 20 — x changed
Because *p resolves to the original variable, you can both read from and write to it. This is how Go achieves indirection without needing special “reference” types outside of pointers.
Dereferencing a nil pointer panics:
A pointer variable that hasn’t been assigned holds the zero value nil. Attempting to dereference nil causes a runtime panic:
var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
Always ensure a pointer is non-nil before dereferencing it, or use the new function / composite literals to initialize the backing value.
The relationship between & and * is symmetric in a simple case: *(&x) gives you x again. Understanding that symmetry is the core mental model. Think of & as “where is it?” and * as “what’s there?”
A working pointer round-trip:
If you can take the address of a variable and then dereference it to get the original value back, your pointer fundamentals are correct. This round-trip is a quick sanity check:
x := 100
if *(&x) == x {
fmt.Println("round-trip works")
}
Where these operators appear in real code
The & operator is common when you need to pass a large struct to a function without copying it, or when a function expects a pointer receiver (methods). The * operator appears inside those functions to read or modify the original value. Together they underpin Go’s “pass by value with pointer workaround” pattern.
Operator Precedence
When an expression contains more than one operator, precedence determines how the compiler groups the subexpressions. Without it, a + b * c would be ambiguous—should the addition happen first, or the multiplication? Go resolves this with a fixed precedence table that matches what you’d expect from basic arithmetic and boolean logic.
The precedence table
Operators at the top of the table bind tighter than those below. Operators on the same level have equal precedence and are evaluated from left to right.
| Precedence | Operators |
|---|---|
| Highest | * / % << >> & &^ |
+ - | ^ | |
== != < <= > >= | |
&& | |
| Lowest | || |
Unary operators (&, *, +, -, !, ^) have higher precedence than any binary operator, though unary * (pointer dereference) and unary & (address-of) are separate from their binary counterparts and always apply to the adjacent operand.
How precedence plays out in practice
Because * (level 5) outranks + (level 4), a + b * c groups as a + (b * c). The bitwise operators follow the same table: & is level 5 while | and ^ are level 4, so a | b & c is a | (b & c), not (a | b) & c. The comparison operators sit below addition, so a + b == c is (a + b) == c.
a, b, c := 2, 3, 5
fmt.Println(a + b * c) // 17 (b*c then +a)
fmt.Println(a | b & c) // 3 (b&c -> 1, then 2|1)
fmt.Println(a + b == c) // true (2+3 == 5)
Why parentheses still matter
The table is predictable, but reading code that leans heavily on it demands mental effort. Use parentheses even when they aren’t technically required if they make the intent clearer. The compiler does not punish you for them, and they eliminate entire categories of bugs.
// These two lines mean the same thing, but the second requires no mental lookup.
val := x<<1 + y>>2
val := (x << 1) + (y >> 2)
Bitwise vs. logical precedence:
A common surprise for newcomers: bitwise & has higher precedence than ==, but && has lower precedence than ==. So a & b == 0 is a & (b == 0), not (a & b) == 0. Meanwhile a == 0 && b == 0 groups as (a == 0) && (b == 0). When mixing bitwise and relational operators, parentheses are your friend.
Order of Evaluation
Precedence determines grouping—which operands belong to which operator. Order of evaluation determines the sequence in which those operands are actually computed. A common mistake is to assume that precedence also controls execution order; it does not.
What Go guarantees
Go specifies a strict evaluation order for function calls, method calls, and communication operations (channel sends/receives) within an expression, assignment, or return statement: they are evaluated in lexical left-to-right order.
package main
import "fmt"
func a() int { fmt.Println("a"); return 1 }
func b() int { fmt.Println("b"); return 2 }
func main() {
_ = a() + b()
// Output is always:
// a
// b
}
Even though a() and b() are operands of the + operator, the language requires a() to run before b() because it appears leftmost in the source text.
What Go does NOT guarantee
For any operand that is not a function call, method call, or channel operation, the evaluation order is unspecified. The compiler may compute x before y or y before x—even inside the same expression that contains a function call with a guaranteed order.
x := 1
y := 2
result := add(x, y) + y // Is x evaluated before y? It's not guaranteed.
In this example, add will certainly be called after both x and y are evaluated (because its arguments need values), but the order in which x and y themselves are read relative to each other is not specified. If reading those variables had observable side effects, the behavior would be unpredictable.
Never rely on unspecified evaluation order:
Writing code whose correctness depends on whether the compiler fetches x before y or vice versa is a recipe for silent, non-portable bugs. The spec leaves this intentionally open so that compilers can reorder reads for efficiency. The only safe stance is to split expressions that depend on a particular sequence into separate statements.
Short-circuit evaluation
The logical operators && and || also impose a specific order: they evaluate their left operand first. If the result is already determined, the right operand is never evaluated.
false && expensiveFunction() // expensiveFunction will never run
true || riskyOperation() // riskyOperation is skipped
This is guaranteed and can be used intentionally to guard against expensive or unsafe operations. However, writing cheapCheck() && cheapCheck2() and relying on the left-to-right short-circuit is perfectly idiomatic.
Sequence points through assignments and comma
Go does not have a comma operator in the C sense, but the comma in a variable declaration or assignment has an implicit sequence point: the right-hand side of = is fully evaluated before any assignment to the left-hand side begins. In a multi-value assignment, all right-hand expressions are evaluated (in left-to-right order if they are function calls) before assignments happen. This prevents overlaps that could cause data races on the same variable.
x, y := 1, 2
x, y = y, x // swap works: right side is evaluated first, then assignments occur
Summary
The address and pointer operators let you work with memory locations directly, forming the foundation for passing references and building data structures. Operator precedence removes ambiguity from multi-operator expressions, and a quick mental model—“the table is stable, but parentheses are clearer”—keeps code safe. Order of evaluation completes the picture: Go provides guarantees where side effects are observable (function calls, channels, logical short-circuit) and deliberately leaves other ordering unspecified to allow compiler optimizations.
When these three pieces come together in a single expression—say, a pointer dereference inside a parenthesised subexpression that also calls a function—understanding each rule individually is the only way to predict what the program will actually do.