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 == aalways 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
NaNand never panics. Integer division or remainder by a zero divisor panics at run time withinteger 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 >> 2is-4. -
Shifting by the operand’s width or more is not undefined as in C: the result is
0, or-1for 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 << sillegal (a shift of a float) whilevar a int32 = 1 << scomputes inint32.
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: |
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 |
no |
interfaces |
yes, but may panic: see below |
no |
slices, maps, functions |
only against |
no |
type parameters |
if constrained by |
if constrained by |
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 needsstrings.EqualFold,strings.ToLower, or thegolang.org/x/text/collatepackage.strings.Compare(a, b)returns -1, 0 or +1 but is almost never better than the operators; thecmppackage (cmp.Compare,cmp.Less,cmp.Or) is the generic equivalent. -
NaNbreaks reflexivity:x == xis false for a floating-pointNaN, so aNaNcan never be found in a slice by value andmath.IsNaNis required. ANaNused as a map key creates entries that cannot be looked up or deleted (onlyclearremoves 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 whenaholds anint. -
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 onlyany. 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:
-
&xyields a pointer tox. 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. -
*pdereferences a pointer. Dereferencing a nil pointer panics at run time withinvalid memory address or nil pointer dereference. For field and method access Go inserts the dereference for you:p.Titlemeans(*p).Title. -
<-chreceives from a channel and is an expression; the formv, ok := <-chreports whether the value was actually sent (okis false on a closed, drained channel). The sendch <- vis 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
Source: strings: Builder and testing: AllocsPerRun
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
uintis no longer necessary. -
Neither covers the
min,maxandclearbuilt-ins (Go 1.21), ornew(expr)(Go 1.26), which replaces the pointer-to-literal helper functions the books recommend. -
Both predate
anysatisfyingcomparable(Go 1.20) and, in Vivien’s case, generics altogether; the type-constraint operators<and+are expressed today throughcmp.Orderedand 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 ( |
Go 1.18 |
|
Go 1.20 |
interface types satisfy |
Go 1.21 |
|
Go 1.22 |
range over integers ( |
Go 1.26 |
|
Go 1.27 |
no change to the operators; the spec header reads "Language version go1.27" |