beginner Step 3 of 15

Control Flow and Loops

Go Programming

Control Flow and Loops

Go's control flow is intentionally simple. There is only one loop construct — the for loop — which handles all iteration patterns (traditional, while-style, infinite, and range-based). The if statement supports an initialization statement, and the switch statement does not fall through by default (eliminating a common source of bugs in C-like languages). Go also provides defer for cleanup operations, which is one of its most distinctive features.

If Statements

package main

import "fmt"

func main() {
    age := 25

    // Basic if-else
    if age >= 18 {
        fmt.Println("Adult")
    } else {
        fmt.Println("Minor")
    }

    // If with initialization statement (scoped to the if block)
    if score := calculateScore(); score >= 90 {
        fmt.Println("Excellent!")
    } else if score >= 70 {
        fmt.Println("Good")
    } else {
        fmt.Println("Needs improvement")
    }
    // score is NOT accessible here
}

func calculateScore() int { return 85 }

Switch Statement

// Switch — no fall-through by default (no break needed!)
day := "Tuesday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("Weekday")
case "Saturday", "Sunday":
    fmt.Println("Weekend")
default:
    fmt.Println("Unknown")
}

// Switch without expression (like if-elseif)
score := 85
switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
default:
    fmt.Println("C or below")
}

// Type switch
func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("Integer: %d", v)
    case string:
        return fmt.Sprintf("String: %s", v)
    case bool:
        return fmt.Sprintf("Boolean: %t", v)
    default:
        return "Unknown type"
    }
}

For Loops (Go's Only Loop)

// Traditional for loop
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style (just a condition)
count := 0
for count < 5 {
    fmt.Println(count)
    count++
}

// Infinite loop
for {
    fmt.Println("Press Ctrl+C to stop")
    break  // Use break to exit
}

// Range over slice
fruits := []string{"apple", "banana", "cherry"}
for index, fruit := range fruits {
    fmt.Printf("%d: %s
", index, fruit)
}

// Range over map
scores := map[string]int{"Alice": 95, "Bob": 87}
for name, score := range scores {
    fmt.Printf("%s: %d
", name, score)
}

// Range over string (iterates runes, not bytes)
for i, ch := range "Hello 🌍" {
    fmt.Printf("Index %d: %c
", i, ch)
}

// Skip index with blank identifier
for _, fruit := range fruits {
    fmt.Println(fruit)
}

Defer

// defer delays execution until the surrounding function returns
func readFile(path string) (string, error) {
    file, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer file.Close()  // Guaranteed to run when function exits

    content, err := io.ReadAll(file)
    if err != nil {
        return "", err
    }
    return string(content), nil
}

// Multiple defers execute in LIFO order (last-in, first-out)
func example() {
    defer fmt.Println("First deferred")
    defer fmt.Println("Second deferred")
    defer fmt.Println("Third deferred")
    fmt.Println("Main code")
}
// Output: Main code, Third deferred, Second deferred, First deferred
Pro tip: Use defer immediately after acquiring a resource (opening a file, acquiring a lock, starting a timer) to ensure cleanup happens regardless of how the function exits. This pattern eliminates an entire class of resource leak bugs.

Key Takeaways

  • Go has only one loop construct — for — which handles traditional, while-style, infinite, and range-based iteration.
  • Switch does not fall through by default; no break needed (use fallthrough explicitly if needed).
  • The if statement supports an initialization statement: if x := compute(); x > 0 { }.
  • Use defer for cleanup operations — it runs when the enclosing function returns, in LIFO order.
  • Use range to iterate over slices, maps, strings, and channels with clean syntax.