07 / 20 · Day 3
Day 3 · Concept 07

Control flow

Go has one loop keyword (for) and one branch keyword (if), both without parentheses around their conditions. The surprise is defer — schedule something to run when the function returns. switch cases take conditions, not just constants — it's the idiomatic dispatch tool.


1 · The intuition

Go threw out what wasn't needed. There's no whilefor handles both. No do-while — you write for { ...; if ...{ break } }. No ?: ternary — write the if. No exceptions — panic/recover exist but are reserved for "impossible" states.

The whole vocabulary. if, else, for, break, continue, switch, case, default, fallthrough, defer, return, goto, panic, recover. Thirteen keywords. That's the entire flow toolkit.

2 · Try it — the four forms of for

go main.go · for, four ways
package main

import "fmt"

func main() {
    // 1. Classic three-part — like C
    for i := 0; i < 3; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()

    // 2. While-style — single condition
    n := 5
    for n > 0 {
        fmt.Print(n, " ")
        n--
    }
    fmt.Println()

    // 3. Infinite — break out
    count := 0
    for {
        count++
        if count == 3 { break }
    }
    fmt.Println("counted:", count)

    // 4. Range — over slices, maps, strings, channels
    xs := []string{"a", "b", "c"}
    for i, v := range xs {
        fmt.Printf("%d=%s ", i, v)
    }
    fmt.Println()
}

3 · if with an initialiser — the half-statement

Go's if can declare a variable scoped to the if/else block. This is how the (value, err) pattern reads idiomatically:

go main.go · if with initialiser
package main

import (
    "fmt"
    "os"
)

func main() {
    // Classic shape — declare in the if header
    if f, err := os.Open("nonexistent.txt"); err != nil {
        fmt.Println("err:", err)
    } else {
        defer f.Close()
        // use f here
    }
    // f is out of scope here

    // Common variant — pre-existing variable
    n := 5
    if doubled := n * 2; doubled > 5 {
        fmt.Println("big:", doubled)
    }
}

4 · switch — the power tool

Go's switch doesn't fall through by default. Cases can be lists, can be expressions, can omit the discriminant entirely (a switch on true is a clean if/else chain).

go main.go · switch flavours
package main

import "fmt"

func main() {
    // 1. Value switch
    x := 3
    switch x {
    case 1, 2: fmt.Println("small")  // multiple values, one case
    case 3, 4: fmt.Println("medium")
    default:   fmt.Println("large")
    }

    // 2. No discriminant — switch on conditions
    age := 25
    switch {
    case age < 13: fmt.Println("kid")
    case age < 20: fmt.Println("teen")
    case age < 60: fmt.Println("adult")
    default:       fmt.Println("senior")
    }

    // 3. Type switch (sneak peek for day 4)
    var v interface{} = 42
    switch t := v.(type) {
    case int:    fmt.Println("int:", t)
    case string: fmt.Println("str:", t)
    default:     fmt.Println("other")
    }
}

5 · defer — schedule cleanup

go main.go · defer LIFO
package main

import "fmt"

func main() {
    defer fmt.Println("1 last")
    defer fmt.Println("2 second-last")
    defer fmt.Println("3 first")
    fmt.Println("body")
}
// defers run in LIFO order: last deferred runs first.
Defer evaluates arguments now, runs later. defer fmt.Println("x=", x) captures x's current value. To see the final value, wrap in a closure: defer func() { fmt.Println("x=", x) }().

6 · break/continue with labels

go main.go · labelled break
package main

import "fmt"

func main() {
    grid := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    target := 5

outer:
    for i, row := range grid {
        for j, v := range row {
            if v == target {
                fmt.Printf("found at (%d, %d)
", i, j)
                break outer  // breaks the OUTER loop
            }
        }
    }
}

Labels are rare in idiomatic Go — usually you extract the inner loop to a function and return. But for tight inner loops where extraction would obscure the algorithm (a matrix search like above), labels are clean.

7 · From the wild

go kubernetes · informer cache filter (paraphrased)
func filterPods(pods []*Pod, ns string) []*Pod {
    var result []*Pod
    for _, p := range pods {
        switch {
        case p.Namespace != ns:
            continue
        case p.Status.Phase == PodSucceeded:
            continue
        case p.Status.Phase == PodFailed:
            continue
        }
        result = append(result, p)
    }
    return result
}
From the wild: github.com/kubernetes/kubernetes · Apache 2.0

8 · Coming from another language?

If you know…The bridge
C / C++ / JavaNo parens around conditions. switch doesn't fall through. defer is new — closest is RAII or try-finally.
PythonNo elif — just else if. No list comprehensions — explicit for. A bare switch replaces long elif chains.
JavaScriptNo ternary. No do-while. defertry/finally but stacks LIFO across multiple defers.
RustNo match patterns on structs — Go's switch is value-based. defer is closest to Drop but explicit.

9 · Common mistakes

  • Putting defer inside a loop. Defers stack up — they only run when the function returns. for i := 0; i < 1000; i++ { defer f.Close() } leaks 1000 file handles until the function ends.
  • Expecting switch to fall through. It doesn't. Use fallthrough if you really need it (rare).
  • Modifying a loop variable expecting persistence. for i := 0; i < 10; i++ creates a fresh i each iteration in Go 1.22+; older versions share.
  • Forgetting the comma-ok form for channels. v, ok := <-ch tells you if the channel is closed. v := <-ch on a closed channel returns the zero value silently.
  • Using goto. It exists. Don't. The state machine you think you need can be written as a switch.

10 · Exercises (~10 min)

  1. FizzBuzz with a single switch on conditions (no chained if/else).
  2. Defer trace. Predict the output of defer fmt.Println(i) inside a 3-iteration loop. Run it. Now wrap in a closure to capture the live value.
  3. Find in matrix. Reproduce the labelled-break exercise from section 6 with a 4×4 grid and a target value.
  4. Range over a string. for i, r := range "héllo" — what type is r? What's i after the special character? Hint: byte index, not rune index.

11 · When it clicks

  • You never type while.
  • You reach for switch over chained else if by reflex.
  • defer f.Close() appears right after f, err := os.Open(...) without thinking.
  • You can predict what a defer + closure combo will print before running it.
Found this useful?