CHAPTER 08 · CORE LANGUAGE

Operators & Expressions

Operators are the verbs of an expression. Go has the usual arithmetic and logical operators, plus a few quirks worth knowing, like &^ (AND-NOT), ++ being a statement (not an expression), and the absence of a ternary ? :.

Learning objectives

  • Use arithmetic, comparison, logical, and bitwise operators correctly.
  • Understand integer-division behavior and modulo with negatives.
  • Know that ++ and -- are statements, not expressions.
  • Read the precedence table well enough to add parentheses preemptively.
  • Recognize the &, *, and <- operators that we'll explain later (pointers and channels).

Arithmetic operators

OpMeaningWorks on
+addnumbers, strings (concatenation)
-subtract / negatenumbers
*multiplynumbers
/dividenumbers
%modulo (remainder)integers
package main

import "fmt"

func main() {
    a, b := 10, 3
    fmt.Println(a+b, a-b, a*b)   // 13 7 30
    fmt.Println(a/b)             // 3   ← integer division
    fmt.Println(a%b)             // 1

    // Same with floats, full precision
    fmt.Println(10.0 / 3.0)      // 3.3333333333333335

    // String concat
    fmt.Println("Hello, " + "World")
}
!
Integer division truncates toward zero 5 / 2 is 2, not 2.5. -7 / 2 is -3, not -4. The modulo operator's sign follows the dividend: -7 % 2 == -1. Convert to a float (or use math.Mod) when you need real division.

Comparison operators

All return a bool:

OpMeaning
== / !=equal / not equal
< / >less / greater
<= / >=less or equal / greater or equal

Strings compare lexicographically by byte value:

fmt.Println("apple" < "banana")    // true
fmt.Println("apple" == "Apple")    // false (case-sensitive)

Slices and maps cannot be compared with == (only against nil). For deep equality, use reflect.DeepEqual or a per-type loop.

Logical & short-circuit

OpMeaning
&&logical AND
||logical OR
!logical NOT

Both && and || short-circuit: the right operand is only evaluated if necessary. This is critical for safe nil checks:

if u != nil && u.IsAdmin {
    // u.IsAdmin only evaluated when u != nil, no nil deref panic
}
if cached, ok := lookupCache(key); ok || expensiveCheck(key) {
    // expensiveCheck only runs when the cached lookup says false
}

Bitwise operators

Operate on the binary bits of integers:

OpMeaning
&AND
|OR
^XOR (binary) / NOT (unary)
&^AND-NOT (clear bits)
<<left shift
>>right shift
package main

import "fmt"

func main() {
    x, y := 0b0110, 0b0011    // 6 and 3 in binary

    fmt.Printf("%04b\n", x & y)   // 0010   AND
    fmt.Printf("%04b\n", x | y)   // 0111   OR
    fmt.Printf("%04b\n", x ^ y)   // 0101   XOR
    fmt.Printf("%04b\n", x &^ y)  // 0100   AND-NOT (x with bits set in y cleared)

    fmt.Printf("%04b\n", x << 1)  // 1100   shift left
    fmt.Printf("%04b\n", x >> 1)  // 0011   shift right
}

&^ is unique to Go (most languages need x & ~y). It clears the bits of x that are set in y. Handy when you have flag bitmasks.

Assignment & compound

= assigns. The compound forms combine an arithmetic or bitwise op with assignment:

x := 10
x += 5    // x = x + 5  → 15
x -= 3    // 12
x *= 2    // 24
x /= 4    // 6
x %= 5    // 1

bits := uint8(0b1010)
bits |= 0b0001     // set the lowest bit
bits &= 0b0011     // mask to lowest 2 bits
bits ^= 0xFF       // invert all bits

Increment & decrement

x++ and x-- exist, but they are statements, not expressions. You can't use them inside bigger expressions:

i := 0
i++             // ✓ statement
// y := i++    // ❌ won't compile

This rules out the C/Java pattern arr[i++], which Go considers a readability hazard.

Address-of, dereference, and channel operators (preview)

Three operators we'll meet properly later but mention here so you recognize them in code:

  • &x: take the address of x. Produces a pointer (Chapter 16).
  • *p: dereference pointer p. Produces the underlying value (Chapter 16).
  • <-ch: receive a value from channel ch (Chapter 25).
  • ch <- v: send v on channel ch.

Precedence (only 5 levels, easy to memorize)

Go has a deliberately small precedence table. Fewer levels means fewer surprises. From highest to lowest:

LevelOperators
5 (highest)* / % << >> & &^
4+ - | ^
3== != < <= > >=
2&&
1 (lowest)||

Unary operators (+, -, !, ^, *, &, <-) bind tightest of all.

x := 1 + 2*3        // 7 , * before +
y := a == b || c    // (a == b) || c
z := !found && ok   // (!found) && ok
When in doubt, parenthesize Compilers don't charge you for parentheses, but readers will charge you for hidden precedence bugs. Wrap mixed-operator expressions for clarity.

No ternary, use if

Go has no cond ? a : b operator. Use a regular if/else (Chapter 9):

var label string
if score >= 60 {
    label = "pass"
} else {
    label = "fail"
}

For one-liners, you can write a tiny helper:

func ifElse[T any](cond bool, a, b T) T {
    if cond { return a }
    return b
}

label := ifElse(score >= 60, "pass", "fail")

(That's a generic function, Chapter 22.)

Check your understanding

Practice exercises

EXERCISE 1

FizzBuzz

The classic. Print the numbers 1 to 30, but for multiples of 3 print "Fizz", multiples of 5 print "Buzz", and multiples of both print "FizzBuzz".

Use the % operator and an if-else if chain.

Show one possible solution
package main

import "fmt"

func main() {
    for i := 1; i <= 30; i++ {
        switch {
        case i%15 == 0:
            fmt.Println("FizzBuzz")
        case i%3 == 0:
            fmt.Println("Fizz")
        case i%5 == 0:
            fmt.Println("Buzz")
        default:
            fmt.Println(i)
        }
    }
}

The "tagless switch" idiom (next chapter!) is cleaner than if-else if for this kind of cascade.

EXERCISE 2

Bit-flag toggling

Imagine a permissions byte where bit 0 is "read", bit 1 is "write", bit 2 is "execute". Write functions set, clear, and has that take a permission byte and a flag byte and return the new state (or a bool).

Show one possible solution
package main

import "fmt"

const (
    Read    uint8 = 1 << iota   // 0b001
    Write                       // 0b010
    Execute                     // 0b100
)

func set(p, flag uint8) uint8   { return p | flag }
func clear(p, flag uint8) uint8 { return p &^ flag }
func has(p, flag uint8) bool    { return p&flag != 0 }

func main() {
    var perm uint8
    perm = set(perm, Read|Write)
    fmt.Printf("%03b\n", perm)         // 011
    fmt.Println(has(perm, Read))       // true
    fmt.Println(has(perm, Execute))    // false
    perm = clear(perm, Write)
    fmt.Printf("%03b\n", perm)         // 001
}

Further reading

Operators in the bag.