Control Flow in Go

Go has a minimal set of control flow statements: if, for, and switch. There's no while loop or ternary operator.

If Statements

Basic If

if x > 10 {
    fmt.Println("x is greater than 10")
}

Note: Braces {} are always required, and the opening brace must be on the same line.

If-Else

if x > 10 {
    fmt.Println("greater")
} else {
    fmt.Println("less or equal")
}

If-Else If-Else

if x > 10 {
    fmt.Println("greater than 10")
} else if x > 5 {
    fmt.Println("greater than 5")
} else {
    fmt.Println("5 or less")
}

If with Initialization Statement

Go allows a short statement before the condition. The variable is scoped to the if block:

if err := doSomething(); err != nil {
    fmt.Println("Error:", err)
    return
}
// err is not accessible here

// Common pattern: checking map values
if value, ok := myMap[key]; ok {
    fmt.Println("Found:", value)
} else {
    fmt.Println("Not found")
}

// Type assertion
if str, ok := value.(string); ok {
    fmt.Println("It's a string:", str)
}

For Loops

Go has only one looping construct: for. It covers all use cases.

Traditional For Loop

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

While-Style Loop

// Omit init and post statements
i := 0
for i < 10 {
    fmt.Println(i)
    i++
}

Infinite Loop

for {
    // Runs forever until break or return
    if condition {
        break
    }
}

For-Range Loop

The most common way to iterate over collections:

// Slices and arrays
nums := []int{1, 2, 3, 4, 5}
for index, value := range nums {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

// Ignore index
for _, value := range nums {
    fmt.Println(value)
}

// Ignore value (just need index)
for index := range nums {
    fmt.Println(index)
}

// Strings (iterates over runes, not bytes)
for index, runeValue := range "Hello, 世界" {
    fmt.Printf("%d: %c\n", index, runeValue)
}

// Maps
m := map[string]int{"a": 1, "b": 2}
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

// Just keys
for key := range m {
    fmt.Println(key)
}

// Channels
ch := make(chan int)
for value := range ch {
    fmt.Println(value)  // Receives until channel is closed
}

Break and Continue

// Break exits the loop
for i := 0; i < 10; i++ {
    if i == 5 {
        break  // Exit loop when i is 5
    }
    fmt.Println(i)
}

// Continue skips to next iteration
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue  // Skip even numbers
    }
    fmt.Println(i)  // Only prints odd numbers
}

Labels for Nested Loops

outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer  // Breaks out of both loops
        }
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

// Also works with continue
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if j == 1 {
            continue outer  // Skips to next i
        }
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

Switch Statements

Go's switch is more powerful and flexible than in many languages.

Basic Switch

switch day {
case "Monday":
    fmt.Println("Start of work week")
case "Friday":
    fmt.Println("TGIF!")
case "Saturday", "Sunday":  // Multiple values
    fmt.Println("Weekend!")
default:
    fmt.Println("Midweek")
}

Note: No break needed! Go's switch cases don't fall through by default.

Switch with Initialization

switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("macOS")
case "linux":
    fmt.Println("Linux")
default:
    fmt.Println(os)
}

Switch with No Condition

A switch without a condition is the same as switch true. This is a clean way to write long if-else chains:

t := time.Now()
switch {
case t.Hour() < 12:
    fmt.Println("Good morning!")
case t.Hour() < 17:
    fmt.Println("Good afternoon!")
default:
    fmt.Println("Good evening!")
}

Fallthrough

If you need C-style fallthrough (rare):

switch n {
case 1:
    fmt.Println("One")
    fallthrough  // Continue to next case
case 2:
    fmt.Println("Two")  // Prints for both 1 and 2
case 3:
    fmt.Println("Three")
}

Type Switch

Used with interfaces to check the underlying type:

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %s\n", v)
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    case []int:
        fmt.Printf("Slice of ints: %v\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

describe(42)        // Integer: 42
describe("hello")   // String: hello
describe(true)      // Boolean: true
describe([]int{1})  // Slice of ints: [1]
describe(3.14)      // Unknown type: float64

Defer

defer schedules a function call to run after the current function returns. Deferred calls are executed in LIFO (last-in, first-out) order.

Basic Defer

func main() {
    defer fmt.Println("world")
    fmt.Println("hello")
}
// Output:
// hello
// world

Common Use Cases

// Resource cleanup
func readFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // Will run when function returns

    // Read file...
    return nil
}

// Mutex unlocking
func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

// Timing functions
func process() {
    start := time.Now()
    defer func() {
        fmt.Printf("Took %v\n", time.Since(start))
    }()
    // Do work...
}

Defer Gotchas

// Arguments are evaluated immediately
func main() {
    x := 10
    defer fmt.Println(x)  // Prints 10, not 20
    x = 20
}

// But you can use closures to capture current values
func main() {
    x := 10
    defer func() {
        fmt.Println(x)  // Prints 20 (captures variable, not value)
    }()
    x = 20
}

// LIFO order
func main() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)
    }
}
// Output:
// 2
// 1
// 0

Defer with Named Return Values

func double(x int) (result int) {
    defer func() {
        result *= 2  // Modifies the return value
    }()
    return x
}

fmt.Println(double(5))  // 10, not 5

Panic and Recover

Go doesn't have exceptions. For truly exceptional situations, there's panic and recover.

Panic

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

When a panic occurs:

  1. Function execution stops
  2. Deferred functions are executed
  3. Control returns to the caller
  4. Process repeats up the stack
  5. Program crashes with a stack trace

Recover

recover catches a panic and returns normal execution:

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    panic("something went wrong")
}

func main() {
    safeOperation()
    fmt.Println("Program continues")  // This runs
}

When to Use Panic

  • Truly unrecoverable errors (programming bugs, corrupted state)
  • During initialization when a critical resource can't be loaded
  • Never for normal error handling (use error returns instead)
// Example: configuration that must exist
func mustGetEnv(key string) string {
    value := os.Getenv(key)
    if value == "" {
        panic(fmt.Sprintf("required environment variable %s not set", key))
    }
    return value
}

Goto (Rarely Used)

Go has goto, but it's rarely needed:

func example() {
    i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }
}

Generally avoid goto; use for, break, and continue instead.

Next Steps

Continue to 04-functions.md to learn about functions, multiple return values, and closures.