Agentic AI Engineering with Python: Live Course
GoVariables and Data Types

Operators

Go's operator set holds almost no surprises for anyone who has written C, Java, or JavaScript. The interesting parts are the three things Go leaves out, one operator it borrowed from nowhere else, and a couple of behaviours that differ from what you might assume. So rather than listing symbols you already know, this page front-loads the differences.

Three things Go does not have

No ternary operator

max := a > b ? a : b        // does not exist in Go

Write the branch:

max := b
if a > b {
    max = a
}

Since Go 1.21 the built-in max and min functions cover the common numeric case:

largest := max(a, b)
smallest := min(3, 7, 2)

The team's reasoning for omitting ?: was that nested ternaries are unreadable and there is no way to allow the simple form without allowing the nested one. Whether you agree or not, this is settled.

No operator overloading

+ means addition for numbers and concatenation for strings, and those are the only meanings it will ever have. Your own types cannot redefine it.

type Vector struct{ X, Y float64 }

v3 := v1 + v2               // compile error

func (v Vector) Add(o Vector) Vector {    // write a method instead
    return Vector{v.X + o.X, v.Y + o.Y}
}

No increment as an expression

++ and -- exist, but they are statements, not expressions. They cannot appear inside a larger expression and there is no prefix form.

i++                 // fine, a statement on its own
x := i++            // compile error
++i                 // compile error
arr[i++] = value    // compile error

This removes an entire category of "what order does this evaluate in" puzzles.

Arithmetic

a, b := 17, 5

fmt.Println(a + b)    // 22
fmt.Println(a - b)    // 12
fmt.Println(a * b)    // 85
fmt.Println(a / b)    // 3, integer division truncates
fmt.Println(a % b)    // 2, remainder

Two behaviours to note.

Integer division truncates toward zero. 17 / 5 is 3, not 3.4, and -17 / 5 is -3, not -4. To get a fractional result, convert first:

fmt.Println(float64(a) / float64(b))    // 3.4

Modulo takes the sign of the dividend. -7 % 3 is -1 in Go, where some languages give 2. If you need a always-positive result:

func mod(a, b int) int {
    return ((a % b) + b) % b
}

Integer division by zero panics. Float division by zero gives infinity. Guard integer divisors when the value comes from outside your code:

if divisor == 0 {
    return 0, errors.New("division by zero")
}

% works only on integers. For floating point remainder use math.Mod.

Comparison

==   !=   <   <=   >   >=

All six return a bool, and none of them do any implicit conversion. Comparing an int with an int64 is a compile error until you convert one of them.

What can be compared with == is worth knowing precisely:

TypeComparable with ==?
numbers, strings, booleansyes
pointersyes, compares addresses
channelsyes
interfacesyes, compares dynamic type and value
structsyes, if every field is comparable
arraysyes, if the element type is comparable
slicesno, only against nil
mapsno, only against nil
functionsno, only against nil

Struct comparison is a small gift:

type Point struct{ X, Y int }

fmt.Println(Point{1, 2} == Point{1, 2})    // true

Field by field, no method needed. But add a slice field and the whole struct stops being comparable:

type Config struct {
    Name string
    Tags []string        // now Config cannot use ==
}

For those, reflect.DeepEqual works, or in Go 1.21 and later slices.Equal and maps.Equal for the individual fields. In tests, google/go-cmp is the usual choice.

Logical

&&   ||   !

Both && and || short circuit, evaluating the right operand only when the result is still undecided. This is what makes nil checks safe:

if user != nil && user.IsActive {
    // user.IsActive is never evaluated when user is nil
}

if cached != "" || fetchFromDB() != "" {
    // fetchFromDB is never called when cached has a value
}

Both operands must already be bool. There is no truthiness, so if count && ready does not compile when count is a number.

Bitwise, and the one Go invented

a, b := 12, 10     // 1100 and 1010 in binary

fmt.Println(a & b)     // 8   1000  AND
fmt.Println(a | b)     // 14  1110  OR
fmt.Println(a ^ b)     // 6   0110  XOR
fmt.Println(a &^ b)    // 4   0100  AND NOT
fmt.Println(a << 2)    // 48        left shift
fmt.Println(a >> 2)    // 3         right shift

&^ is Go's own contribution, called AND NOT or bit clear. a &^ b clears every bit in a that is set in b. Other languages spell it a & ~b, and Go gives it a single operator because clearing flags is common enough to deserve one.

const (
    Read = 1 << iota
    Write
    Execute
)

perms := Read | Write | Execute   // 111
perms = perms &^ Write            // 101, Write removed

Shifts are also a fast way to multiply or divide by powers of two, though the compiler already does that optimisation, so write the shift only when you actually mean bits.

Assignment operators

Every binary operator has a compound form:

x := 10

x += 5      // 15
x -= 3      // 12
x *= 2      // 24
x /= 4      // 6
x %= 4      // 2
x <<= 3     // 16
x |= 1      // 17
x &^= 1     // 16

+= also works on strings, though in a loop strings.Builder is the better choice.

Precedence

Go has five levels of binary operator precedence, which is fewer than most languages and easier to hold in your head:

   5    *   /   %   <<   >>   &   &^
   4    +   -   |   ^
   3    ==  !=  <   <=   >    >=
   2    &&
   1    ||

Unary operators bind tightest of all. Everything at the same level associates left to right.

The one that catches people is that & and | sit at the arithmetic levels, well above the comparisons:

if flags & Read != 0 {      // parses as flags & (Read != 0), a type error

Go's parser will reject that with a confusing message. Parenthesise bitwise tests:

if flags&Read != 0 {        // gofmt writes it tightly, and it means (flags&Read) != 0

gofmt uses spacing to signal precedence. It writes a*b + c rather than a * b + c, tightening the higher precedence operation. Once you notice this, the formatter is quietly documenting the parse for you on every line.

Pointer and channel operators

Two more that are technically operators, both covered properly in later sections:

p := &value        // address of
v := *p            // dereference

ch <- 42           // send to a channel
x := <-ch          // receive from a channel

A worked example

Everything on this page, doing something small and real:

func classify(score int) string {
    switch {
    case score >= 90 && score <= 100:
        return "excellent"
    case score >= 75:
        return "good"
    case score >= 50:
        return "pass"
    case score >= 0:
        return "fail"
    default:
        return "invalid"
    }
}

func isEven(n int) bool {
    return n&1 == 0        // faster than n%2 == 0, and clearer about intent
}

func average(values []int) float64 {
    if len(values) == 0 {
        return 0
    }
    total := 0
    for _, v := range values {
        total += v
    }
    return float64(total) / float64(len(values))
}

That last function shows the conversion habit in its natural home. Dividing two integers would truncate, so both operands are converted before the division, not after.

Next, let's finish this section with fmt, the package that turns all these values back into text you can read.

How is this guide?

Last updated on