Go has only a handful of control structures and no parentheses around their conditions. The body braces are always required.

if

if x > 0 {
    ...
} else if x == 0 {
    ...
} else {
    ...
}

if may include a short statement that scopes a variable to the if/else chain only — extremely common for error handling:

if err := doSomething(); err != nil {
    return err
}
// err is not in scope here

for — the only loop

for is the only looping construct. It has four forms:

// 1. Classic 3-clause C-style
for i := 0; i < 10; i++ { ... }

// 2. While-style
for cond { ... }

// 3. Infinite
for { ... }       // break to exit

// 4. Range
for i, v := range slice { ... }
for k, v := range mapVar { ... }
for i := range arr { ... }       // ignore the value
for _, v := range arr { ... }    // ignore the index
for r := range chanVar { ... }   // receive until channel closes

break exits the innermost loop. continue jumps to the next iteration.

Labeled break / continue

outer:
for i := 0; i < 5; i++ {
    for j := 0; j < 5; j++ {
        if i*j > 6 {
            break outer
        }
    }
}

Loop variable capture (post Go 1.22 fix)

Before Go 1.22 the loop variable was shared across iterations, which broke goroutines and closures. Since Go 1.22 each iteration gets a fresh variable, so this works as you'd expect:

for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }() // 0, 1, 2 (in some order) on Go 1.22+
}

For older Go you had to write i := i inside the loop.

switch

Go's switch doesn't fall through by default. Each case is independent.

switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
    fmt.Println("weekday")
case "Sat", "Sun":
    fmt.Println("weekend")
default:
    fmt.Println("?")
}

You can omit the expression for a clean if/else chain:

switch {
case x < 0:
    sign = -1
case x == 0:
    sign = 0
default:
    sign = 1
}

fallthrough forces execution into the next case (rarely used):

switch n {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("one or two")
}

Type switch (preview, see Chapter 7)

switch v := x.(type) {
case int:
    ...
case string:
    ...
default:
    ...
}

defer

defer schedules a call to run when the surrounding function returns (normally or via panic). Deferred calls run in LIFO order.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()        // guaranteed to run on every return path

    // ... use f ...
    return nil
}

Important rules:

  • Arguments to a deferred call are evaluated immediately, but the call itself runs at function return:

go i := 1 defer fmt.Println("i =", i) // prints "i = 1" i = 99

  • Multiple defers run in LIFO:

go defer fmt.Println("A") defer fmt.Println("B") defer fmt.Println("C") // prints C, B, A

goto

Exists for unusual control flow inside a single function, but is rarely seen in idiomatic code (mostly used in generated parsers).

panic and recover (preview)

func safe() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("boom")
}

panic should be reserved for truly unrecoverable bugs. Use error returns for normal failures (Chapter 7).

Run the example

go run ./part1-language/examples/05_control