Skip to content

Control Flow Beginner

Go's control flow is minimal by design: one loop keyword (for), no parentheses around conditions, and switches that don't fall through by default. Fewer constructs means fewer bugs.


if / else

No parentheses around conditions. Braces are always required.

if x > 0 {
    fmt.Println("positive")
} else if x < 0 {
    fmt.Println("negative")
} else {
    fmt.Println("zero")
}

if with Init Statement

Declare a variable scoped to the if/else block. This is extremely common for error handling.

if err := doSomething(); err != nil {
    log.Fatal(err)
}
// err is NOT accessible here

if val, ok := cache[key]; ok {
    return val
}

Interview Tip

"Go's if with init statement is idiomatic for the comma-ok pattern and error checks. The variable is scoped to the if/else chain, keeping the outer scope clean. You'll see if err := f(); err != nil in virtually every Go codebase."


switch

Expression Switch

No break needed -- Go switches do not fall through by default.

switch day {
case "Monday":
    fmt.Println("Start of week")
case "Friday":
    fmt.Println("Almost weekend")
case "Saturday", "Sunday": // multiple values in one case
    fmt.Println("Weekend")
default:
    fmt.Println("Midweek")
}

Switch with Init Statement

switch os := runtime.GOOS; os {
case "linux":
    fmt.Println("Linux")
case "darwin":
    fmt.Println("macOS")
default:
    fmt.Printf("Other: %s\n", os)
}

Tagless Switch (replaces if/else chains)

switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
case score >= 70:
    grade = "C"
default:
    grade = "F"
}

fallthrough

Forces execution to continue into the next case (unconditionally). Rarely used.

switch n := 3; {
case n > 0:
    fmt.Println("positive") // printed
    fallthrough
case n > 10:
    fmt.Println("big?")     // also printed (unconditional fallthrough)
}
// Output: positive
//         big?

fallthrough is unconditional

Unlike C's fall-through, Go's fallthrough does not re-evaluate the next case condition. It always enters the next case body. This catches people off guard.

Type Switch

Determine the dynamic type of an interface value. Covered in depth in the intermediate section on type assertions.

func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("bool: %t", v)
    default:
        return fmt.Sprintf("unknown: %T", v)
    }
}

for Loop

Go has only for -- no while, no do-while. The for keyword covers all loop patterns.

C-Style (Three-Component)

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

While-Style (Condition Only)

n := 1
for n < 100 {
    n *= 2
}

Infinite Loop

for {
    line, err := reader.ReadString('\n')
    if err != nil {
        break
    }
    process(line)
}

range Over Collections

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

// Slice / Array -- index and value
nums := []int{10, 20, 30}
for i, v := range nums {
    fmt.Printf("index=%d value=%d\n", i, v)
}

// Index only
for i := range nums {
    fmt.Println(i)
}

// Value only (discard index)
for _, v := range nums {
    fmt.Println(v)
}
// Map -- key and value (iteration order is random)
config := map[string]string{"host": "localhost", "port": "8080"}
for k, v := range config {
    fmt.Printf("%s=%s\n", k, v)
}

// Key only
for k := range config {
    fmt.Println(k)
}
// String -- index and rune (NOT byte)
for i, r := range "Go语言" {
    fmt.Printf("byte_offset=%d rune=%c\n", i, r)
}
// byte_offset=0 rune=G
// byte_offset=1 rune=o
// byte_offset=2 rune=语
// byte_offset=5 rune=言
// Channel -- receives until channel is closed
ch := make(chan int)
go func() {
    for _, v := range []int{1, 2, 3} {
        ch <- v
    }
    close(ch)
}()
for v := range ch {
    fmt.Println(v)
}
// Integer range (Go 1.22+)
for i := range 5 {
    fmt.Println(i) // 0, 1, 2, 3, 4
}

Range Reference Table

Type First Value Second Value Notes
[]T, [N]T index int element T Copy of element
map[K]V key K value V Random order
string byte offset int rune rune Decodes UTF-8
chan T element T -- Blocks until closed
int (Go 1.22+) int -- 0 to n-1

break, continue, and Labels

Basic break and continue

for i := 0; i < 10; i++ {
    if i == 5 {
        break    // exit the loop entirely
    }
    if i%2 == 0 {
        continue // skip to next iteration
    }
    fmt.Println(i) // 1, 3
}

Labeled Statements

Labels let you break or continue an outer loop from inside a nested loop.

outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break outer // breaks the outer loop, not just inner
            }
            fmt.Printf("(%d,%d) ", i, j)
        }
    }
// Output: (0,0) (0,1) (0,2) (1,0)
// Label with continue
rows:
    for _, row := range matrix {
        for _, val := range row {
            if val < 0 {
                continue rows // skip entire row
            }
        }
        processRow(row)
    }

Interview Tip

"Labeled break/continue is the idiomatic way to exit nested loops in Go. It's cleaner than boolean flags and more controlled than goto. It's commonly used in parser and matrix-processing code."


goto

Go supports goto but with restrictions: you cannot jump over variable declarations or into other blocks.

func example() {
    n := 0
    if n == 0 {
        goto done
    }
    fmt.Println("skipped")
done:
    fmt.Println("done")
}

Avoid goto in almost all cases

goto exists in Go primarily for machine-generated code and very specific cleanup patterns. In practice, labeled break/continue or early return handles every case more clearly. Using goto in interview code signals poor design judgment.


Quick Reference

Construct Syntax Go Difference
if with init if x := f(); x > 0 { } Init var scoped to if/else
switch switch x { case 1: ... } No fall-through by default
Tagless switch switch { case x > 0: ... } Replaces if/else chains
Type switch switch v := i.(type) { } Runtime type checking
C-style for for i := 0; i < n; i++ { } Only loop keyword
While-style for condition { } No while keyword
Infinite loop for { } Common for servers
Range (slice) for i, v := range s { } Copies elements
Range (map) for k, v := range m { } Random order
Range (string) for i, r := range s { } Decodes runes
Range (int) for i := range n { } Go 1.22+
Labeled break outer: for ... { break outer } Exit outer loop
fallthrough case 1: fallthrough Unconditional

Best Practices

  1. Use if init statements for error handling -- if err := f(); err != nil { } keeps scope tight.
  2. Prefer tagless switch over long if/else chains -- it's cleaner and more readable.
  3. Always handle the default case in switches that process external input.
  4. Use labeled break for nested loops instead of boolean flags or goto.
  5. Use for range instead of manual indexing when you don't need to modify the index.
  6. Prefer early returns over deep nesting -- flatten your control flow.

Common Pitfalls

Range loop variable reuse (pre-Go 1.22)

Before Go 1.22, the loop variable was reused across iterations. Capturing it in a goroutine without copying was a classic bug.

// BUG (Go < 1.22): all goroutines print the last value
for _, v := range values {
    go func() {
        fmt.Println(v) // captures shared variable
    }()
}

// FIX (Go < 1.22): shadow the variable
for _, v := range values {
    v := v // create new variable per iteration
    go func() {
        fmt.Println(v)
    }()
}
Go 1.22+ fixes this by creating a new variable per iteration by default.

Range copies elements

range gives you a copy of each element, not a reference.

type Item struct{ Value int }
items := []Item{{1}, {2}, {3}}

for _, item := range items {
    item.Value *= 2 // modifies the COPY, not the slice
}
// items unchanged: [{1} {2} {3}]

// Fix: use index
for i := range items {
    items[i].Value *= 2
}

Map iteration order is random

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
    fmt.Print(k) // different order each run
}
If you need sorted keys, sort them explicitly with slices.Sorted(maps.Keys(m)).

fallthrough skips condition checks

x := 5
switch {
case x > 3:
    fmt.Println("greater than 3")
    fallthrough
case x < 2: // NOT evaluated -- fallthrough is unconditional
    fmt.Println("this always prints after fallthrough")
}

Performance Considerations

  • range vs index loop: Performance is identical for slices of basic types. For large structs, range copies each element -- use index-based access or a slice of pointers to avoid copies.
  • switch vs if/else: The compiler optimizes both similarly. switch is preferred for readability, not performance.
  • Map range allocation: Iterating over a map allocates internally. For hot paths, consider pre-extracting keys into a slice.
  • Break early: In search loops, break on first match. Don't scan the entire collection when you have the answer.

Key Takeaways

  1. Go has only for for loops -- it covers C-style, while-style, infinite, and range patterns.
  2. switch does not fall through by default -- no break needed (opposite of C/Java).
  3. if and switch support init statements for scoped variable declarations.
  4. range over strings yields runes (Unicode code points), not bytes.
  5. range copies elements -- use index-based access to modify slice elements in place.
  6. Go 1.22+ supports range over integers and fixes the loop variable capture bug.
  7. Use labeled break/continue for nested loops; avoid goto.