Operators and Expressions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2) and was checked on 2026-10-18. Every go.mod used to compile the examples has go 1.27, and every example was formatted with gofmt, passed go vet, compiled and run for real; the output shown is the output that was produced. Go’s operators are deliberately few and conventional: there is no operator overloading, no ternary operator, no implicit numeric conversion between operands, and ++ and -- are statements rather than expressions. What makes Go’s expressions precise is the specification: integer overflow wraps, the order of evaluation is defined where it matters, and comparability is a property of the type that the compiler checks. The types the operators work on are described in Types and Values; the statements that consume boolean expressions are in Control Flow. Readers coming from C will find the same family of operators in the C Reference, and from Rust in Rust Types and Values; the differences are called out below.

The operators at a glance

The specification groups the operators and defines the precedence of the binary ones in Operator precedence. Unary operators bind tighter than every binary operator. Binary operators of the same precedence associate from left to right.

Precedence Operators Meaning

5

* / % << >> & &^

multiplication, division, remainder, shifts, bitwise AND, bit clear (AND NOT)

4

+ - | ^

addition or concatenation, subtraction, bitwise OR, bitwise XOR

3

== != < <= > >=

comparison, giving an untyped boolean

2

&&

logical AND, short-circuiting

1

||

logical OR, short-circuiting

unary

+ - ! ^ * & <-

identity, negation, logical NOT, bitwise complement, pointer dereference, address-of, channel receive

Go has five binary precedence levels where C has more than a dozen, and the bitwise operators sit with the arithmetic ones rather than below the comparisons. That removes one of the classic C traps, discussed under Precedence and associativity. Assignment (=, := and the compound forms such as +=) and ++/-- are not part of the expression grammar at all: they are statements, covered in Assignment and increment/decrement.

Arithmetic operators

The arithmetic operators +, -, * and / apply to numeric types and + also concatenates strings; % and the bitwise operators apply only to integers. Both operands must have the same type (or one must be an untyped constant that converts to the other’s type): Go never converts silently, so int + int64 or int * float64 is a compile error, not a promotion. The rules are in Arithmetic operators.

Three behaviours deserve attention:

  • Integer division truncates toward zero and the remainder takes the sign of the dividend, so (a/b)*b + a%b == a always holds. This differs from Python and Ruby, whose % follows the divisor, and matches C99 and Java.

  • Integer overflow wraps around at run time for signed and unsigned types. There is no trap and no undefined behaviour, unlike signed overflow in C. The wrapped value is the one the two’s-complement representation gives, and it is well defined by the spec ("Integer overflow"). The single signed division that overflows, the most negative value divided by -1, does not panic either: it yields the dividend.

  • Floating-point arithmetic follows IEEE 754: division by zero gives an infinity or NaN and never panics. Integer division or remainder by a zero divisor panics at run time with integer divide by zero.

package main

import (
	"fmt"
	"math"
)

func main() {
	// Integer division truncates toward zero; the remainder takes the sign
	// of the dividend, so (a/b)*b + a%b == a always holds.
	for _, p := range [][2]int{{7, 2}, {-7, 2}, {7, -2}, {-7, -2}} {
		a, b := p[0], p[1]
		fmt.Printf("%2d / %2d = %2d   %2d %% %2d = %2d\n", a, b, a/b, a, b, a%b)
	}

	// Integer overflow wraps around silently at run time.
	var small int8 = math.MaxInt8
	small++
	var u uint8
	u--
	fmt.Println("int8 127+1 =", small, " uint8 0-1 =", u)

	// The one signed division that overflows: MinInt64 / -1 does not panic.
	m, minus1 := int64(math.MinInt64), int64(-1)
	fmt.Println("MinInt64 / -1 =", m/minus1, " MinInt64 % -1 =", m%minus1)

	// Floating point follows IEEE 754: no panic on division by zero.
	zero := 0.0
	fmt.Println(1/zero, -1/zero, zero/zero, zero/zero == zero/zero)

	// Complex numbers support + - * / and ==.
	c := complex(1, 2) * complex(3, -1)
	fmt.Println(c, real(c), imag(c))

	// Integer division by zero is a run-time panic.
	defer func() { fmt.Println("recovered:", recover()) }()
	d := 0
	fmt.Println(10 / d)
}
 7 /  2 =  3    7 %  2 =  1
-7 /  2 = -3   -7 %  2 = -1
 7 / -2 = -3    7 % -2 =  1
-7 / -2 =  3   -7 % -2 = -1
int8 127+1 = -128  uint8 0-1 = 255
MinInt64 / -1 = -9223372036854775808  MinInt64 % -1 = 0
+Inf -Inf NaN false
(5+5i) 5 5
recovered: runtime error: integer divide by zero

When both operands are constants the compiler evaluates the expression at compile time with arbitrary precision, and it rejects a constant division by zero and a constant that does not fit its target type. These are errors, not wrapping:

package main

import "fmt"

func main() {
	fmt.Println(10 / 0)
}
main.go:6:19: invalid operation: division by zero
package main

import (
	"fmt"
	"math"
)

func main() {
	var i int32 = math.MaxInt32 + 1
	var u uint = -1
	var b byte = 255 + 1
	fmt.Println(i, u, b, 5.5%2)
}
main.go:9:16: cannot use math.MaxInt32 + 1 (untyped int constant 2147483648) as int32 value in variable declaration (overflows)
main.go:10:15: cannot use -1 (untyped int constant) as uint value in variable declaration (overflows)
main.go:11:15: cannot use 255 + 1 (untyped int constant 256) as byte value in variable declaration (overflows)
main.go:12:23: invalid operation: operator % not defined on 5.5 (untyped float constant)

Code that must detect overflow at run time uses explicit range checks or the helpers in math/bits (bits.Add64, bits.Mul64 and friends return a carry or the high word), or the math constants such as math.MaxInt64 in a comparison before the operation. Floating-point values are not for money: use integer cents or a decimal library, as Types and Values explains.

The int type is 64 bits wide on 64-bit platforms and has been since Go 1.1; the language only promises that it is at least 32 bits, so write int64 when the width matters. Complex numbers (complex64, complex128) support +, -, *, /, == and !=, with the built-ins complex, real and imag, as the first example shows.

Bitwise operators and shifts

The bitwise operators work on integers and operate bit by bit: & (AND), | (OR), ^ (XOR), &^ (AND NOT, "bit clear") and the unary ^ (complement, which C spells ~). a &^ b clears in a every bit that is set in b, which is the idiomatic way to remove a flag from a bit set. Because Go has no ~ unary operator on integers (the symbol ~T in a constraint is something else, see Generics), the complement of an unsigned x is ^x and for signed integers ^x equals -x - 1.

package main

import "fmt"

func main() {
	const a, b uint8 = 0b1100_1010, 0b1010_0110
	fmt.Printf("a      %08b\n", a)
	fmt.Printf("b      %08b\n", b)
	fmt.Printf("a & b  %08b  (AND)\n", a&b)
	fmt.Printf("a | b  %08b  (OR)\n", a|b)
	fmt.Printf("a ^ b  %08b  (XOR)\n", a^b)
	fmt.Printf("a &^ b %08b  (AND NOT, bit clear)\n", a&^b)
	fmt.Printf("^a     %08b  (unary ^ is bitwise NOT)\n", ^a)
	fmt.Printf("-5 as ^4: %d, ^0 = %d\n", ^4, ^0)

	// Shifts: left fills with zeros; right shift of a signed value copies the
	// sign bit (arithmetic shift); of an unsigned value it fills with zeros.
	var s int8 = -16
	var us uint8 = 240
	fmt.Println(s>>2, us>>2, s<<1, us<<1)

	// A shift count at or beyond the width gives 0 (or -1 for a negative signed value).
	var n uint = 70
	fmt.Println(uint64(1)<<n, int64(-8)>>n, int64(8)>>n)

	// Since Go 1.13 the shift count may be a signed integer.
	// A negative count panics at run time.
	k := 3
	fmt.Println(1<<k, 256>>k)
	defer func() { fmt.Println("recovered:", recover()) }()
	k = -1
	fmt.Println(1 << k)
}
a      11001010
b      10100110
a & b  10000010  (AND)
a | b  11101110  (OR)
a ^ b  01101100  (XOR)
a &^ b 01001000  (AND NOT, bit clear)
^a     00110101  (unary ^ is bitwise NOT)
-5 as ^4: -5, ^0 = -1
-4 60 -32 224
0 -1 0
8 32
recovered: runtime error: negative shift amount

The shift operators << and >> behave as follows:

  • The left operand is the value, the right operand the shift count. Since Go 1.13 the count may be of any integer type, signed or unsigned; before then it had to be unsigned, and code written for older releases is full of uint(n) conversions that are no longer needed. A negative count panics at run time; a negative constant count is a compile error.

  • << fills with zeros. >> on an unsigned value fills with zeros; on a signed value it replicates the sign bit (an arithmetic shift), so -16 >> 2 is -4.

  • Shifting by the operand’s width or more is not undefined as in C: the result is 0, or -1 for a negative signed value shifted right.

  • A shift of a constant by a constant is exact; a non-constant shift of an untyped constant is subtle: the constant takes the type it would have had if the shift were replaced by its left operand. That makes var f float64 = 1 << s illegal (a shift of a float) while var a int32 = 1 << s computes in int32.

package main

import "fmt"

func main() {
	var s uint = 33

	// An untyped constant in a non-constant shift takes the type it would
	// have if the shift were replaced by its left operand alone.
	var a int32 = 1 << s // 1 is an int32 here: 1<<33 overflows to 0
	var b int64 = 1 << s // 1 is an int64 here
	var c = 1 << s       // 1 is an int
	fmt.Println(a, b, c)
	fmt.Printf("%T %T %T\n", a, b, c)

	// In a constant shift the operands are exact; the result stays untyped.
	const big = 1 << 100
	fmt.Println(big >> 98)
}
0 8589934592 8589934592
int32 int64 int
4
package main

import "fmt"

func main() {
	var s uint = 3
	var f float64 = 1 << s
	fmt.Println(f)
	fmt.Println(1 << -1)
}
main.go:7:18: invalid operation: shifted operand 1 (type float64) must be integer
main.go:9:19: invalid operation: negative shift count -1 (untyped int constant)

go vet includes a shift analyzer for the case the compiler accepts but that always loses bits, such as shifting an 8-bit value by 8; the report appears together with other findings later on this page in vet reports for operators. For bit counting, rotation and byte reversal use math/bits (bits.OnesCount, bits.RotateLeft, bits.LeadingZeros) rather than hand-written shifts.

Comparison operators and comparability

== and != test equality; <, <=, > and >= test order. The result of any comparison is an untyped boolean value, which is why type Flag bool; var f Flag = x == y compiles. The rules, in Comparison operators, hinge on two properties of the type:

  • a type is comparable if == is defined for it;

  • a type is ordered if < is defined for it.

Type Comparable with == Ordered with <

booleans

yes

no

integers, floats, complex numbers

yes (floats: NaN is never equal, not even to itself)

integers and floats yes; complex no

strings

yes (byte-wise)

yes (lexicographic by byte)

pointers

yes (equal if same address or both nil)

no

channels

yes (same channel or both nil)

no

arrays

yes if the element type is comparable

no

structs

yes if every field is comparable (blank _ fields are ignored)

no

interfaces

yes, but may panic: see below

no

slices, maps, functions

only against nil

no

type parameters

if constrained by comparable (or a type set of comparable types)

if constrained by cmp.Ordered or an equivalent type set

Both operands must be assignable to a common type, so int == int64, int == float64 and a defined type against its underlying type with a typed operand are compile errors. An untyped constant converts to the other operand’s type, which is why isbn == "x" is fine while isbn == s for a string variable s is not.

package main

import (
	"fmt"
	"math"
	"strings"
)

type ISBN string

type Loan struct {
	Book   ISBN
	Member string
	Days   int
}

type Point [2]int

func main() {
	// Numbers, strings, booleans, pointers, channels, interfaces, structs of
	// comparable fields and arrays of comparable elements are comparable.
	l1 := Loan{"9780134190440", "ada", 14}
	l2 := Loan{"9780134190440", "ada", 14}
	fmt.Println("structs:", l1 == l2, l1 != Loan{})
	fmt.Println("arrays: ", Point{1, 2} == Point{1, 2}, [3]string{"a"} == [3]string{"a", "", ""})
	fmt.Println("pointers:", &l1 == &l1, &l1 == &l2)

	// Strings are ordered byte-wise; compare with < <= > >=.
	fmt.Println("strings:", "Go" < "Rust", "apple" < "Apple", "é" > "z", strings.Compare("a", "b"))

	// NaN is unordered: every comparison with it is false except !=.
	nan := math.NaN()
	fmt.Println("NaN:", nan == nan, nan < 1, nan > 1, nan != nan)

	// Interface values are equal when dynamic types and values are equal.
	var a, b any = 1, 1
	var c any = int64(1)
	var n1, n2 any
	fmt.Println("interfaces:", a == b, a == c, n1 == n2, a == nil)

	// Comparison of an interface with a concrete value converts the value.
	fmt.Println("mixed:", a == 1, a == int64(1), c == 1)

	// A map key or == on interface values holding uncomparable dynamic
	// types panics at run time.
	defer func() { fmt.Println("recovered:", recover()) }()
	var x, y any = []int{1}, []int{1}
	fmt.Println(x == y)
}
structs: true true
arrays:  true true
pointers: true false
strings: true false true -1
NaN: false false false true
interfaces: true false true false
mixed: true false false
recovered: runtime error: comparing uncomparable type []int

Several things in that output are worth stating explicitly:

  • Struct and array equality is field-by-field and element-by-element; there is no identity comparison. A pointer comparison (&l1 == &l2) tests addresses, not contents.

  • "apple" < "Apple" is false because comparison is by byte value and uppercase letters sort first in ASCII and UTF-8. Ordering by locale or case-insensitively needs strings.EqualFold, strings.ToLower, or the golang.org/x/text/collate package. strings.Compare(a, b) returns -1, 0 or +1 but is almost never better than the operators; the cmp package (cmp.Compare, cmp.Less, cmp.Or) is the generic equivalent.

  • NaN breaks reflexivity: x == x is false for a floating-point NaN, so a NaN can never be found in a slice by value and math.IsNaN is required. A NaN used as a map key creates entries that cannot be looked up or deleted (only clear removes them).

  • An interface value is equal to another if both are nil or if both hold identical dynamic types and equal dynamic values. A value of a concrete type compared with an interface is first converted to the interface type, which is why a == int64(1) is false when a holds an int.

  • If the two dynamic types are identical but not comparable, the comparison panics at run time: runtime error: comparing uncomparable type []int. The compiler cannot catch it, because the static type is only any. The same panic occurs when such a value is used as a map key.

The specification calls the types in the last two rows of the table "not comparable". The compiler reports them with precise messages:

package main

import "fmt"

type Book struct {
	Title   string
	Authors []string
}

func main() {
	a, b := []int{1}, []int{1}
	fmt.Println(a == b)

	m1, m2 := map[string]int{}, map[string]int{}
	fmt.Println(m1 == m2)

	f := func() {}
	fmt.Println(f == f)

	b1, b2 := Book{}, Book{}
	fmt.Println(b1 == b2)

	set := map[Book]bool{}
	fmt.Println(set)
}
main.go:12:14: invalid operation: a == b (slice can only be compared to nil)
main.go:15:14: invalid operation: m1 == m2 (map can only be compared to nil)
main.go:18:14: invalid operation: f == f (func can only be compared to nil)
main.go:21:14: invalid operation: b1 == b2 (struct containing []string cannot be compared)
main.go:23:13: invalid map key type Book
package main

import "fmt"

type ISBN string

func main() {
	var i int = 1
	var f float64 = 1
	var n int64 = 1
	var isbn ISBN = "x"
	fmt.Println(i == f)
	fmt.Println(i == n)
	fmt.Println(isbn == "x", isbn == string("x"))
	var s string = "x"
	fmt.Println(isbn == s)
	fmt.Println(i == nil)
	fmt.Println(true < false)
}
main.go:12:19: invalid operation: i == f (mismatched types int and float64)
main.go:13:19: invalid operation: i == n (mismatched types int and int64)
main.go:14:35: invalid operation: isbn == string("x") (mismatched types ISBN and string)
main.go:16:22: invalid operation: isbn == s (mismatched types ISBN and string)
main.go:17:19: invalid operation: i == nil (mismatched types int and untyped nil)
main.go:18:14: invalid operation: true < false (operator < not defined on untyped bool)

To compare slices, maps and structs that contain them, use the library: slices.Equal, slices.EqualFunc, maps.Equal, bytes.Equal for byte slices, reflect.DeepEqual for arbitrary values (slow, and it treats a nil slice as different from an empty one), and, in tests, go-cmp. strings.EqualFold compares strings ignoring case. See Collections and Utility Packages.

The comparable constraint

In generic code the predeclared constraint comparable (Go 1.18) admits the types on which == and != are defined. Go 1.20 relaxed it: an interface type such as any now satisfies comparable, because the comparison is well defined for it, even though it may panic at run time for an uncomparable dynamic type. Before 1.20 Index([]any{...}, x) failed to compile with any does not satisfy comparable.

package main

import (
	"cmp"
	"fmt"
	"slices"
)

// Index uses the comparable constraint: T must support == and !=.
func Index[T comparable](xs []T, want T) int {
	for i, x := range xs {
		if x == want {
			return i
		}
	}
	return -1
}

func main() {
	fmt.Println(Index([]string{"a", "b"}, "b"))
	fmt.Println(Index([]float64{1, 2}, 2))

	// Since Go 1.20 an interface type satisfies comparable; the comparison
	// can still panic at run time if a dynamic type is not comparable.
	fmt.Println(Index([]any{1, "x", 2.5}, any("x")))
	defer func() { fmt.Println("recovered:", recover()) }()
	fmt.Println(Index([]any{[]int{1}}, any([]int{1})))
}

func init() {
	// cmp.Ordered covers the types that support < <= > >=.
	fmt.Println(cmp.Compare(2, 10), cmp.Compare("b", "a"), slices.Max([]string{"ant", "bee"}))
}
-1 1 bee
1
1
1
recovered: runtime error: comparing uncomparable type []int

The last line of the program is printed first because it runs in an init function; see Packages, Imports and Initialization. Ordering in generic code uses the cmp.Ordered constraint.

Logical operators

&&, || and ! operate on booleans only. There is no truthiness: if n { for an integer, or !n, is a compile error (see Control Flow). && and || are short-circuit operators: the right operand is evaluated only if the left one does not already decide the result. Use that for guards (len(s) > 0 && s[0] == 'x', p != nil && p.ok). && binds tighter than ||, so a || b && c means a || (b && c).

package main

import "fmt"

func check(name string, v bool) bool {
	fmt.Println("  evaluated", name)
	return v
}

func main() {
	fmt.Println("false && ...:")
	_ = check("A", false) && check("B", true) // B is skipped

	fmt.Println("true || ...:")
	_ = check("C", true) || check("D", false) // D is skipped

	fmt.Println("mixed, && binds tighter than ||:")
	r := check("E", false) || check("F", true) && check("G", true)
	fmt.Println("result:", r)

	// Typical use: a guard before a dereference or an index.
	var names []string
	if len(names) > 0 && names[0] == "Ada" {
		fmt.Println("found")
	}
	fmt.Println(!true, !(1 > 2))
}
false && ...:
  evaluated A
true || ...:
  evaluated C
mixed, && binds tighter than ||:
  evaluated E
  evaluated F
  evaluated G
result: true
false true

Address, dereference and receive operators

Three unary operators connect values to storage and channels:

  • &x yields a pointer to x. The operand must be addressable: a variable, a pointer indirection, a slice index expression, a field selector of an addressable struct, or an index of an addressable array. As a special case &T{...} takes the address of a composite literal. Map elements are not addressable (the map may move them when it grows), nor are function results, constants or literals.

  • *p dereferences a pointer. Dereferencing a nil pointer panics at run time with invalid memory address or nil pointer dereference. For field and method access Go inserts the dereference for you: p.Title means (*p).Title.

  • <-ch receives from a channel and is an expression; the form v, ok := <-ch reports whether the value was actually sent (ok is false on a closed, drained channel). The send ch <- v is a statement, not an expression, and sending on a receive-only channel is a compile error.

Since Go 1.26 the built-in new accepts an expression, new(int64(5)), returning a pointer to a variable initialized with that value; it replaces the helper functions such as func ptr[T any](v T) *T that programs used to declare for optional fields.

package main

import "fmt"

type Book struct {
	ISBN  string
	Title string
}

func main() {
	b := Book{"9780134190440", "The Go Programming Language"}
	p := &b // address-of: p has type *Book
	p.Title = "TGPL"
	fmt.Println(b.Title, (*p).Title == p.Title)

	// A composite literal is the one non-variable operand &x accepts.
	q := &Book{ISBN: "1"}
	n := &[]int{1, 2}
	fmt.Println(q.ISBN, *n)

	// *p dereferences; *p = v stores through the pointer.
	x := 1
	px := &x
	*px += 41
	fmt.Println(x, *px)

	// new(expr) (Go 1.26) allocates a variable initialised with a value.
	ptr := new(int64(5))
	fmt.Println(*ptr)

	// Receive: <-ch is an expression; ch <- v is a statement.
	ch := make(chan int, 1)
	ch <- 7
	v, ok := <-ch
	fmt.Println(v, ok)
	close(ch)
	v, ok = <-ch
	fmt.Println(v, ok)

	// A nil pointer dereference panics at run time.
	defer func() { fmt.Println("recovered:", recover()) }()
	var np *Book
	fmt.Println(np.Title)
}
TGPL true
1 [1 2]
42 42
5
7 true
0 false
recovered: runtime error: invalid memory address or nil pointer dereference
package main

func f() int { return 1 }

func main() {
	m := map[string]int{"a": 1}
	_ = &m["a"]
	_ = &f()
	_ = &"literal"
	c := make(chan int)
	var r <-chan int = c
	r <- 1
	_ = *m
}
main.go:7:7: invalid operation: cannot take address of m["a"] (map index expression of type int)
main.go:8:7: invalid operation: cannot take address of f() (value of type int)
main.go:9:7: invalid operation: cannot take address of "literal" (untyped string constant)
main.go:12:2: invalid operation: cannot send to receive-only channel <-chan int r (variable of type <-chan int)
main.go:13:7: invalid operation: cannot indirect m (variable of type map[string]int)

Pointers and the value-versus-reference choice are the subject of Pointers and Value Semantics; channels of Channels and Select. Unlike C there is no pointer arithmetic (p + 1 is a compile error); the unsafe package provides it explicitly, see unsafe and Low-Level Programming.

Assignment and increment/decrement

++ and -- are statements that apply to a numeric variable, and only in the postfix form: i++ is shorthand for i += 1. There is no ++i, and j := i++ or f(i++) is a syntax error. Removing the prefix form and the value-producing form removes a class of order-of-evaluation puzzles. Each binary operator op has a compound assignment x op= y, which evaluates the operands of x only once (so t[next()] += 5 calls next once).

package main

import "fmt"

func main() {
	i := 0
	j := i++
	fmt.Println(j)
}
main.go:7:8: syntax error: unexpected ++ at end of statement
package main

import "fmt"

func main() {
	i := 0
	++i
	fmt.Println(i)
}
main.go:7:2: syntax error: unexpected ++, expected }

Tuple assignment assigns several values at once. The index expressions and pointer indirections on the left and all the expressions on the right are evaluated first, in the usual order; then the assignments are carried out left to right. That is why a, b = b, a swaps and why x, y = y, x+y advances a Fibonacci pair without a temporary. The blank identifier _ discards a value.

package main

import "fmt"

func main() {
	// ++ and -- are statements, not expressions; only the postfix form exists.
	i := 0
	i++
	i += 10
	i <<= 2
	i %= 7
	fmt.Println(i)

	// Tuple assignment: all right-hand operands (and index/pointer operands
	// on the left) are evaluated first, then the assignments happen.
	a, b := 1, 2
	a, b = b, a
	fmt.Println(a, b)

	x, y := 0, 1
	for range 10 {
		x, y = y, x+y
	}
	fmt.Println("fib:", x)

	// The spec's example: the index i is evaluated before i is assigned.
	s := []int{10, 20, 30}
	j := 0
	j, s[j] = 1, 99
	fmt.Println(j, s)

	// Swap slice elements and a map entry.
	s[0], s[2] = s[2], s[0]
	m := map[string]int{"a": 1, "b": 2}
	m["a"], m["b"] = m["b"], m["a"]
	fmt.Println(s, m)

	// The blank identifier discards a value.
	_, err := fmt.Sscanf("7", "%d", &i)
	fmt.Println(i, err)

	// Compound assignment evaluates its left operand once.
	idx := 0
	next := func() int { idx++; return idx - 1 }
	t := []int{1, 1, 1}
	t[next()] += 5
	fmt.Println(t, idx)
}
2
2 1
fib: 55
1 [99 20 30]
[30 20 99] map[a:2 b:1]
7 <nil>
[6 1 1] 1

The short variable declaration := has its own rules (at least one new variable on the left, redeclaration only in the same scope) described in Variables, Constants and iota; the mistakes it produces look like this:

package main

import "fmt"

func main() {
	a, b := 1, 2
	a, b := 3, 4
	var c int
	c, d := 5
	if x = 3; x > 0 {
	}
	fmt.Println(a, b, c, d)
}
main.go:7:7: no new variables on left side of :=
main.go:9:10: assignment mismatch: 2 variables but 1 value
main.go:10:5: undefined: x

Precedence and associativity

With five levels there is not much to memorise, and the two traps of C are gone. First, &, | and ^ do not sit below the comparison operators: x&1 == 0 means (x&1) == 0, whereas in C it parses as x & (1 == 0). Second, the shifts have the same precedence as multiplication, so 1<<2+1 is (1<<2)+1, which is 5, not 8. gofmt formats expressions so that spacing hints at the grouping (x&y + 1), and it is a reliable way to check what you meant.

package main

import "fmt"

func main() {
	x, y := 6, 3
	p, q, r := true, false, false

	// Precedence 5: * / % << >> & &^     Precedence 4: + - | ^
	fmt.Println("1+2*3     =", 1+2*3)
	fmt.Println("(1+2)*3   =", (1+2)*3)
	fmt.Println("1+2<<3    =", 1+2<<3, "  // 1 + (2<<3): << binds like *")
	fmt.Println("1<<2+1    =", 1<<2+1, "  // (1<<2) + 1")
	fmt.Println("x&y+1     =", x&y+1, "  // (x&y) + 1")
	fmt.Println("x|y == 7  =", x|y == 7, " // | is level 4, == is level 3")
	fmt.Println("x&1 == 0  =", x&1 == 0, " // & is level 5: no parentheses needed, unlike C")
	fmt.Println("-x*y      =", -x*y, "  // unary operators bind tightest")
	fmt.Println("^x&y      =", ^x&y)
	fmt.Println("p||q&&r  =", p || q && r, "  // p || (q && r): && is level 2, || is level 1")
}
1+2*3     = 7
(1+2)*3   = 9
1+2<<3    = 17   // 1 + (2<<3): << binds like *
1<<2+1    = 5   // (1<<2) + 1
x&y+1     = 3   // (x&y) + 1
x|y == 7  = true  // | is level 4, == is level 3
x&1 == 0  = true  // & is level 5: no parentheses needed, unlike C
-x*y      = -18   // unary operators bind tightest
^x&y      = 1
p||q&&r  = true   // p || (q && r): && is level 2, || is level 1

There is no exponent operator (use math.Pow for floating point and a loop or math/big for integers), no comma operator, no ternary conditional (cond ? a : b; write an if statement, or in generic helper code a small function) and no sizeof operator (the unsafe.Sizeof function covers that). The min, max and clear built-ins (Go 1.21) fill some of the gaps that operators leave:

package main

import (
	"fmt"
	"math"
)

func main() {
	// min and max (Go 1.21) work on any ordered type, including strings.
	fmt.Println(min(3, 1, 2), max(2.5, 1), min("pear", "apple"))

	// If any argument is NaN the result is NaN; -0.0 is less than +0.0.
	fmt.Println(min(1.0, math.NaN()), max(math.Copysign(0, -1), 0))

	// Constant arguments give a constant result.
	const lim = min(10, 20, 5)
	var buf [lim]byte
	fmt.Println(len(buf))

	// clear (Go 1.21) empties a map or zeroes a slice's elements.
	m := map[string]int{"a": 1}
	s := []int{1, 2, 3}
	clear(m)
	clear(s[1:])
	fmt.Println(m, len(m), s)
}
1 2.5 apple
NaN 0
5
map[] 0 [1 0 0]

Order of evaluation

The specification fixes the order only partly (Order of evaluation):

  • all function calls, method calls, receive operations and binary logical operations in an expression are evaluated in the usual lexical left-to-right order;

  • the order between those and the evaluation of plain variables, map indexing and other operands is not specified.

So a + f() may read a before or after f modifies it, and []int{a, f()} may be [1 2] or [2 2]. Real programs must not depend on it; assign to temporaries when a function with side effects appears in an expression that also reads the variable it changes.

package main

import "fmt"

func trace(name string, v int) int {
	fmt.Print(name, " ")
	return v
}

func sum(args ...int) int {
	t := 0
	for _, a := range args {
		t += a
	}
	return t
}

func main() {
	// Function calls, method calls, receive operations and binary logical
	// operations are evaluated in lexical left-to-right order.
	r := trace("a", 1) + trace("b", 2)*trace("c", 3)
	fmt.Println("=", r)

	fmt.Print("args: ")
	fmt.Println("=", sum(trace("x", 1), trace("y", 2), trace("z", 3)))

	// The order of evaluation of a plain variable against a function call is
	// NOT specified. The spec says this may print either [1 2] or [2 2].
	a := 1
	f := func() int { a++; return a }
	x := []int{a, f()}
	fmt.Println("unspecified:", x)

	// Map index order relative to calls is unspecified too, but each
	// operand is evaluated exactly once.
	m := map[string]int{}
	m["k"] += trace("rhs", 5)
	fmt.Println(m)
}
a b c = 7
args: x y z = 6
unspecified: [2 2]
rhs map[k:5]

The line unspecified: [2 2] is what this toolchain happens to print; another compiler version is allowed to print [1 2]. Arguments to a call are evaluated left to right before the call, and defer and go evaluate their function value and arguments at the statement itself (see Functions, Closures and Defer).

String concatenation

+ on strings creates a new string: strings are immutable, so s += w allocates a new buffer and copies both operands every time. A loop that builds a long string with += is therefore quadratic in the result length, with one allocation per iteration. Constant strings are concatenated by the compiler and cost nothing at run time.

The standard remedies are strings.Builder (an amortised-growth byte buffer that converts to a string without copying; call Grow when the final size is known), strings.Join for a slice of strings, and bytes.Buffer or bufio.Writer when the destination is an io.Writer anyway. fmt.Sprintf is convenient for a few values but slower than direct concatenation.

package main

import (
	"fmt"
	"strings"
	"testing"
)

var words = strings.Fields(strings.Repeat("shelf ", 100))

func plus() string {
	s := ""
	for _, w := range words {
		s += w + " "
	}
	return s
}

func builder() string {
	var sb strings.Builder
	sb.Grow(len(words) * 6)
	for _, w := range words {
		sb.WriteString(w)
		sb.WriteByte(' ')
	}
	return sb.String()
}

func join() string { return strings.Join(words, " ") }

func main() {
	const greeting = "Hello, " + "shelf" // constant expression: folded at compile time
	fmt.Println(greeting, len(greeting))

	fmt.Println(plus() == builder()+"", len(plus()))
	fmt.Println("allocs with +=:      ", testing.AllocsPerRun(20, func() { _ = plus() }))
	fmt.Println("allocs with Builder: ", testing.AllocsPerRun(20, func() { _ = builder() }))
	fmt.Println("allocs with Join:    ", testing.AllocsPerRun(20, func() { _ = join() }))
}
Hello, shelf 12
true 600
allocs with +=:       100
allocs with Builder:  1
allocs with Join:     1

The compiler can concatenate a handful of operands in one expression with a single allocation (a + b + c copies once), so the quadratic cost appears in loops. Details of the string representation, ranging by rune and the cost of conversions are in Strings, Runes and UTF-8.

No operator overloading, no implicit conversion

Operators work on the built-in types and on defined types whose underlying type supports them; you cannot define + for your own struct, and there are no operator methods. Defined numeric types keep the operators of their underlying type but are distinct types, so mixing them needs an explicit conversion. The standard example is time.Duration, an int64 count of nanoseconds:

package main

import (
	"fmt"
	"time"
)

func main() {
	n := 3
	fmt.Println(n * time.Second)
}
main.go:10:14: invalid operation: n * time.Second (mismatched types int and time.Duration)
package main

import (
	"fmt"
	"time"
)

func main() {
	n := 3
	// No operator overloading: Duration is an int64 and arithmetic is the
	// ordinary integer kind, so the operand types must match exactly.
	d := time.Duration(n) * time.Second
	fmt.Println(d, 2*time.Second, d+500*time.Millisecond, d/2)
	fmt.Println(d > time.Second, time.Duration(1.5*float64(time.Second)))
}
3s 2s 3.5s 1.5s
true 1.5s

Source: time: Duration

The program shows that 2*time.Second compiles because 2 is an untyped constant, while n*time.Second with an int variable does not. Compare Rust’s operator traits, which allow user-defined +, and C++, which allows overloading broadly; Go chooses named methods (Add, Mul, Equal, Compare) so that every operation is visible at the call site.

Vet reports for operators

Some operator misuse compiles but is almost certainly wrong. go vet reports it, and the checks run automatically as part of go test:

package main

import "fmt"

func main() {
	var b byte = 1
	x := 3
	x = x
	fmt.Println(b<<8, x == 1 || x == 1)
	if x&1 == 0 || x&1 == 0 {
		fmt.Println("even")
	}
	var f func()
	fmt.Println(f == nil, fmt.Sprint)
}
main.go:8:2: self-assignment of x
main.go:9:20: redundant or: x == 1 || x == 1
main.go:10:5: redundant or: x&1 == 0 || x&1 == 0
main.go:14:2: fmt.Println arg fmt.Sprint is a func value, not called
main.go:9:14: b (8 bits) too small for shift of 8

The analyzers behind these lines are assign (self-assignment), bools (redundant or suspect boolean operands), shift (a shift that discards all the bits) and printf (passing an uncalled function to a print function). See Formatting, vet, fix and Static Analysis.

Source: cmd/vet

Where the books are outdated

Learning Go by Bodner (chapter 2, 2021) and Learning Go Programming by Vivien (chapter 2, 2016) predate several changes to operators and expressions:

  • Both predate signed shift counts (Go 1.13). Code that converts counts to uint is no longer necessary.

  • Neither covers the min, max and clear built-ins (Go 1.21), or new(expr) (Go 1.26), which replaces the pointer-to-literal helper functions the books recommend.

  • Both predate any satisfying comparable (Go 1.20) and, in Vivien’s case, generics altogether; the type-constraint operators < and + are expressed today through cmp.Ordered and the type sets of Go 1.18 and later.

  • The statement that every Go type can be tested for equality is incorrect: slices, maps and functions can be compared only to nil, and structs and arrays containing them are not comparable at all. Interface comparison can additionally panic at run time.

Version notes

Release Change that touches this page

Go 1.13

signed integers allowed as shift counts; binary (0b), octal (0o) and hexadecimal-float literals and _ digit separators

Go 1.18

comparable and type sets in constraints, so operators can be required of type parameters

Go 1.20

interface types satisfy comparable; slice-to-array conversions

Go 1.21

min, max and clear built-ins

Go 1.22

range over integers (for i := range n), see Control Flow

Go 1.26

new(expr), taking an initial value

Go 1.27

no change to the operators; the spec header reads "Language version go1.27"

Source: Go 1.13, Go 1.20, Go 1.21 and Go 1.26 release notes