Control flow#

Go keeps the control-flow surface small. if / else, for (the only loop keyword), switch, select for channels, plus break / continue / return / goto / fallthrough and defer for cleanup-on-exit. No exceptions; errors are values (see Errors).

For the values branches consider, see Types.

Conditional#

if / else if / else evaluates branches top-to-bottom. The optional init statement binds a variable scoped to the conditional.

        flowchart TD
    A([start]) --> B{x > 0?}
    B -->|true| C[positive]
    B -->|false| D{x == 0?}
    D -->|true| E[zero]
    D -->|false| F[negative]
    C --> Z([end])
    E --> Z
    F --> Z
    
if x > 0 {
    positive()
} else if x == 0 {
    zero()
} else {
    negative()
}

Init form binds a variable scoped to the conditional.

if v, err := load(); err != nil {
    return err
} else {
    use(v)
}

Go has no ternary expression.

Loops#

for is the only loop keyword. Three forms.

        flowchart TD
    A([init]) --> B{cond?}
    B -->|true| C[body]
    C --> D[step]
    D --> B
    B -->|false| E([end])
    

C-style#

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

While-style#

for queue.Len() > 0 {
    handle(queue.Pop())
}

Infinite#

for {
    select {
    case <-done:
        return
    case v := <-ch:
        handle(v)
    }
}

Range#

range walks slices, arrays, maps, strings, channels, and (Go 1.22+) integers.

Slice or array; binds index and value.

for i, v := range xs {
    fmt.Println(i, v)
}

Map; the iteration order is randomised.

for k, v := range m {
    fmt.Println(k, v)
}

String; binds byte index and rune.

for i, r := range "héllo" {
    fmt.Println(i, r)
}

Channel; runs until the channel is closed.

for v := range ch {
    fmt.Println(v)
}

Integer range (Go 1.22+).

for i := range 10 {
    fmt.Println(i)
}

For Go 1.22+, the per-iteration loop variable is per iteration (new binding each loop); earlier versions reused the same binding, which bit closures over goroutines.

Switch#

switch is the operator’s branch-table form. Cases do not fall through by default (the opposite of C); use fallthrough explicitly when needed.

        flowchart TD
    A([switch state]) --> B{Idle?}
    B -->|yes| C[start]
    B -->|no| D{Running?}
    D -->|yes| E[step]
    D -->|no| F{Done?}
    F -->|yes| G[cleanup]
    F -->|no| H[default]
    C --> Z([end])
    E --> Z
    G --> Z
    H --> Z
    
switch state {
case Idle:
    start()
case Running:
    step()
case Done:
    cleanup()
default:
    return fmt.Errorf("unknown state: %v", state)
}

A switch without an expression is equivalent to switch true; cases are full boolean expressions.

switch {
case x < 0:
    negative()
case x == 0:
    zero()
default:
    positive()
}

Type switch on an interface value.

switch v := x.(type) {
case int:    fmt.Println("int", v)
case string: fmt.Println("string", v)
case nil:    fmt.Println("nil")
default:     fmt.Println("other", v)
}

Select#

select waits on multiple channel operations. The first ready case runs; if several are ready, one is picked at random.

select {
case v := <-ch:
    handle(v)
case ch <- next:
    sent()
case <-ctx.Done():
    return ctx.Err()
case <-time.After(5 * time.Second):
    return errors.New("timeout")
default:
    // runs if no channel is ready (non-blocking)
}

See Concurrency for the goroutine model around select.

Jumps#

Keyword

Effect

break

Exit the innermost for, switch, or select. With a label, break out of an outer loop.

continue

Skip to the next iteration of the innermost for.

return

Exit the current function, optionally with return values.

goto

Jump to a named label in the same function. Rare; the common use is to exit cleanly from deeply nested loops.

fallthrough

In switch, fall into the next case (one level only).

outer: for _, row := range rows {
    for _, cell := range row {
        if cell == target { break outer }
    }
}

defer#

defer schedules a function call to run when the surrounding function returns. Deferred calls run in LIFO order. The operator uses defer for cleanup that must run on every exit path (close files, unlock mutexes, end traces).

f, err := os.Open(path)
if err != nil { return err }
defer f.Close()

mu.Lock()
defer mu.Unlock()

// … operations on f and the locked resource …
return nil

Deferred arguments are evaluated at the defer statement, not at exit; closures capture by reference, so deferring a function that reads a variable sees the latest value.

References#