Polyglot
Languages Go operators
Go § operators

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:

PrecedenceOperators
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; var for 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/else in 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.