Operators
Go’s operator surface is small and C-family. The principal additions: the short variable declaration (:=), the channel send and receive (ch <- v, <-ch), the blank identifier (_) for discarding values. The principal removals: no ternary operator, no overloaded operators, no implicit numeric conversions, no pointer arithmetic. The combination — narrow operator surface, no overloading, explicit conversions — produces deliberate, readable arithmetic at the cost of some C-family conciseness.
Arithmetic
a + b // addition (and string concatenation)
a - b // subtraction
a * b // multiplication
a / b // division (integer for ints, float for floats)
a % b // modulo (integers only)
-a // unary negation
+a // unary plus (rarely used)
Integer division truncates toward zero:
fmt.Println(7 / 2) // 3
fmt.Println(-7 / 2) // -3
fmt.Println(7 % 2) // 1
fmt.Println(-7 % 2) // -1
For floating-point division, both operands must be float:
fmt.Println(7.0 / 2.0) // 3.5
fmt.Println(7.0 / 2) // 3.5 (one float makes both float)
fmt.Println(float64(7) / float64(2)) // 3.5; explicit
There is no ** (power) operator; use math.Pow for floats and explicit multiplication or math/big for integers.
Increment and decrement
x++ // increment
x-- // decrement
++ and -- are statements, not expressions, in Go. They cannot appear in expressions:
y := x++ // ERROR
y := x; x++ // OK
The discipline produces clearer code at the cost of some C-style conciseness.
Comparison
a == b // equal
a != b // not equal
a < b // less than
a > b // greater than
a <= b // less than or equal
a >= b // greater than or equal
Comparison produces a bool value. The operands must be of the same (or assignable) type:
var x int = 5
var y int64 = 5
// x == y // ERROR: type mismatch
x == int(y) // OK
Equality (==, !=) admits comparing:
- Numeric types (same type).
- Strings.
- Booleans.
- Pointers.
- Channels.
- Interfaces (compares the underlying concrete type and value).
- Structs (field-by-field, only if all fields are comparable).
- Arrays (element-by-element, only if elements are comparable).
Slices, maps, and functions are not comparable (with the exception of comparing to nil):
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// s1 == s2 // ERROR: slices are not comparable
s1 == nil // OK; comparing to nil is admitted
// Use slices.Equal (Go 1.21+) for slice comparison:
import "slices"
slices.Equal(s1, s2) // true
For map comparison, maps.Equal (Go 1.21+) is the conventional choice.
Logical operators
a && b // logical AND (short-circuit)
a || b // logical OR (short-circuit)
!a // logical NOT
Both && and || short-circuit — they evaluate the right operand only if necessary:
if p != nil && p.value > 0 { // safe; p.value not accessed if p == nil
// ...
}
if cached || compute() { // compute() called only if !cached
// ...
}
The operands must be bool; Go does not coerce other types:
var n int = 5
if n { // ERROR: int is not bool
}
if n != 0 { // OK
}
The strictness eliminates the C-style truthiness pitfalls.
Bitwise operators
a & b // bitwise AND
a | b // bitwise OR
a ^ b // bitwise XOR
a &^ b // bit clear (AND NOT)
a << n // left shift
a >> n // right shift (arithmetic for signed, logical for unsigned)
^a // unary bitwise NOT
The &^ (bit-clear) operator is distinctive: a &^ b clears the bits of a that are set in b:
fmt.Printf("%b\n", 0b1100 &^ 0b1010) // 0100
The conventional uses are flag manipulation, masking, and packed integer operations.
Assignment operators
a = b // simple assignment
a += b // a = a + b
a -= b // a = a - b
a *= b
a /= b
a %= b
a &= b // a = a & b
a |= b
a ^= b
a &^= b
a <<= b
a >>= b
Multiple assignment is admitted:
a, b = 1, 2 // assign two values
a, b = b, a // swap
x, y, z = 1, 2, 3
The mechanism is conventional for swapping and unpacking multi-return functions.
The short variable declaration :=
The := form declares and initialises a variable, inferring the type:
x := 5 // var x int = 5
name := "Alice" // var name string = "Alice"
result, err := compute() // multi-return form
The := is admitted only inside function bodies; package-level declarations require var or const.
The form admits partial redeclaration: if at least one variable on the left is new, := admits reassignment of the others:
result, err := first() // both new
result, err := second() // OK; result reassigned, err redeclared (?? actually no)
// Actually: NOT OK if both are existing
// OK only if at least one is new
The actual rule: := requires at least one new variable on the left; existing variables are assigned (not redeclared). The mechanism is conventional in error-handling chains:
result, err := step1()
if err != nil { return err }
result, err := step2(result) // err re-assigned; result re-assigned
// ERROR: no new variable
The conventional defence: introduce intermediate names or use = after the initial declaration:
result, err := step1()
if err != nil { return err }
result, err = step2(result) // = (not :=) — both already declared
if err != nil { return err }
Address and dereference
p := &x // address of x
v := *p // dereference; the value pointed to
Treated in Pointers and references.
Channel operators
ch <- value // send to channel
v := <-ch // receive from channel
v, ok := <-ch // two-value receive (ok=false if closed)
close(ch) // close channel
The <- is the principal channel operator; treated in Concurrency.
The blank identifier _
The _ discards a value:
_, err := fmt.Println("hello") // discard the count
import _ "image/png" // import for side effects only
for _, v := range slice { // discard the index
fmt.Println(v)
}
The mechanism admits ignoring values that the language requires the program to acknowledge.
Type assertions and type conversions
Type assertions and conversions are not strictly operators but are part of the Go expression surface:
n := int(x) // type conversion
s := i.(string) // type assertion
s, ok := i.(string) // two-value type assertion
Treated in Types and Methods and interfaces.
Operator precedence
Go has fewer precedence levels than C — five binary precedence levels:
| Precedence | Operators |
|---|---|
| 5 (highest) | *, /, %, <<, >>, &, &^ |
| 4 | +, -, |, ^ |
| 3 | ==, !=, <, <=, >, >= |
| 2 | && |
| 1 (lowest) | || |
Unary operators (+, -, !, ^, *, &, <-) bind tightly to their operand.
The simplification means more operations need parentheses than in C:
// In Go, & and | have the same precedence as *, /:
n := a + b<<2 // a + (b << 2)
n := (a + b) << 2 // explicit
The conventional discipline is to use parentheses freely for clarity.
A note on the absence of ?:
Go does not have a ternary operator. The conventional substitute is if/else:
// In C / many languages:
// max = a > b ? a : b
// In Go:
var max int
if a > b {
max = a
} else {
max = b
}
The deliberate omission is documented in the Go FAQ; the design rationale is that if/else is sufficiently clear and that the ternary’s compactness is not worth the additional grammar.
For a value-returning conditional, an immediately-invoked function admits compactness:
max := func() int {
if a > b { return a }
return b
}()
The form is rare in idiomatic Go; explicit if/else is the conventional choice.
A note on the absence of operator overloading
Go does not admit operator overloading. The + works on numeric types and strings only; user types cannot redefine it.
The conventional defence is named methods:
type Vec2 struct { X, Y float64 }
func (a Vec2) Add(b Vec2) Vec2 {
return Vec2{X: a.X + b.X, Y: a.Y + b.Y}
}
c := a.Add(b) // not a + b
The discipline produces explicit, named operations at the cost of mathematical conciseness.
Common patterns
Multi-return assignment
result, err := compute()
if err != nil {
return err
}
a, b = b, a // swap
values := strings.Split(s, ",")
key, value := values[0], values[1]
Default-on-error
n, err := strconv.Atoi(s)
if err != nil {
n = 0 // default
}
Comma-ok idiom
v, ok := m[key] // map lookup
if !ok {
// key not present
}
s, ok := i.(string) // type assertion
if !ok {
// i is not a string
}
v, ok := <-ch // channel receive
if !ok {
// channel closed
}
The pattern is one of Go’s most distinctive idioms.
Bit flags
const (
FlagRead = 1 << iota // 1
FlagWrite // 2
FlagExecute // 4
)
flags := FlagRead | FlagWrite // 3
hasRead := flags&FlagRead != 0 // true
flags = flags &^ FlagRead // clear FlagRead
Conditional assignment
// "or default" pattern:
host := os.Getenv("HOST")
if host == "" {
host = "localhost"
}
// "if condition then value" pattern:
status := "inactive"
if user.Active {
status = "active"
}
A note on the conventional discipline
The contemporary Go operator advice:
- Use
:=for local declarations;varfor explicit type or zero-value declarations. - Use the comma-ok idiom for map lookups, type assertions, and channel receives.
- Use parentheses freely — Go’s precedence is simpler than C, but readability matters.
- Use
_to discard unwanted values. - Use named methods in place of operator overloading.
- Use
if/elsein place of a ternary; resist the IIFE temptation. - Use bitwise operations sparingly — most code does not need them.
The combination — small operator set, no ternary, no overloading, comma-ok idiom, explicit conversions — is the substance of Go’s expression surface. The discipline produces deliberate, explicit code at the cost of some C-family compactness.