Skip to main content
Go beginner Lesson 6 of 25

Control Flow in Go

Master if/else, switch statements, Go's single for loop, range iteration, and loop control with break, continue, and labels.

if / else

The if statement is the primary tool for conditional execution. Go’s version is standard in structure but has two notable differences from C-family languages: parentheses around the condition are not used, and braces are mandatory even for single-statement bodies. These rules eliminate an entire class of formatting debates and the “dangling else” bug.

x := 42

if x > 100 {
    fmt.Println("big")
} else if x > 10 {
    fmt.Println("medium")
} else {
    fmt.Println("small")
}

Init statement in if

The if statement supports an optional initializer that runs before the condition is evaluated. The key benefit is scope: the variable declared in the initializer is only visible inside the if/else block. This pattern is used constantly with error handling — it keeps the err variable tightly scoped rather than polluting the surrounding function.

// err is scoped to this if/else block — not accessible outside it
if err := doWork(); err != nil {
    log.Printf("work failed: %v", err)
    return err
}

// The same pattern works for map lookups — val and ok are scoped to the block
if val, ok := myMap["key"]; ok {
    fmt.Println("found:", val)
} else {
    fmt.Println("key not present")
}

switch

Go’s switch is cleaner and more powerful than C’s. The most important difference is that cases do not fall through by default — each case automatically breaks when it ends. This eliminates the most common source of switch bugs in C code. Cases can also match multiple values at once, and the switch expression can be omitted entirely to use it as a readable alternative to a long if/else chain.

day := "Tuesday"

switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("Weekday")
case "Saturday", "Sunday":
    fmt.Println("Weekend")
default:
    fmt.Println("Unknown")
}

Expression-less switch (acts like if/else chain)

Omitting the switch expression lets each case be an arbitrary boolean condition. This reads more cleanly than a long chain of else if statements when the conditions don’t all test the same variable.

score := 78

switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 70:
    fmt.Println("C")
default:
    fmt.Println("F")
}

switch with init statement

Like if, a switch can include an init statement. This is useful when the value you’re switching on requires a function call that you don’t want to assign to an outer variable.

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

fallthrough

When you genuinely need C-style fall-through behavior, use the fallthrough keyword explicitly. This makes the intent clear in the code — it cannot happen by accident. Note that fallthrough only falls into the very next case, not all remaining cases.

n := 2
switch n {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two")
    fallthrough
case 3:
    fmt.Println("three")
case 4:
    fmt.Println("four")
}
// Prints: two, three
// Execution falls through to case 3, but NOT case 4

for — Go’s Only Loop

Go unifies all loop patterns under a single for keyword. There is no while, no do/while, no foreach. This simplicity means there is only one construct to learn and only one to read, regardless of which loop pattern is being used.

Classic C-style for loop

The three-clause form — init, condition, post — is used when you need an explicit counter or when you need to step through indices.

for i := 0; i < 5; i++ {
    fmt.Println(i)
}
// 0, 1, 2, 3, 4

While-style loop

Omit the init and post clauses to get a while loop. The loop runs as long as the condition is true.

n := 1
for n < 100 {
    n *= 2
}
fmt.Println(n) // 128

Infinite loop

Omit everything for an infinite loop. This pattern is common in servers and event loops that run until an explicit break or return.

for {
    line, err := readLine()
    if err != nil {
        break
    }
    process(line)
}

range — iterate over collections

range is the idiomatic way to iterate over slices, arrays, maps, strings, and channels. It returns up to two values per iteration — the index (or key) and the value. Use _ to discard whichever you don’t need. For strings, range iterates by Unicode rune rather than by byte, which means it handles multi-byte characters correctly.

// Slice — index and value
fruits := []string{"apple", "banana", "cherry"}
for i, fruit := range fruits {
    fmt.Printf("%d: %s\n", i, fruit)
}

// Ignore index when you only need the value
for _, fruit := range fruits {
    fmt.Println(fruit)
}

// Index only — useful when you need to modify the slice in place
for i := range fruits {
    fmt.Println(i)
}

// Map — key and value (iteration order is intentionally random)
scores := map[string]int{"Alice": 95, "Bob": 87}
for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

// String — iterates by rune (Unicode-safe), not by byte
for i, ch := range "Hello, 世界" {
    fmt.Printf("index %d: %c\n", i, ch)
}

// Channel — receive values until the channel is closed
ch := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch)
}()
for v := range ch {
    fmt.Println(v) // 0, 1, 2
}

// Go 1.22+: range over integer — cleaner than for i := 0; i < n; i++
for i := range 5 {
    fmt.Println(i) // 0, 1, 2, 3, 4
}

break and continue

break and continue give you fine-grained control over loop execution. break exits the loop immediately. continue skips the rest of the current iteration and moves to the next one. Both apply to the innermost enclosing loop by default.

// break exits the innermost loop as soon as the condition is met
for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i) // 0, 1, 2, 3, 4
}

// continue skips even numbers and only prints odds
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(i) // 1, 3, 5, 7, 9
}

Labels for Nested Loop Control

By default, break and continue only affect the innermost loop. Labels let you target an outer loop directly, which is useful when searching through a 2D structure and you want to stop both loops as soon as you find a match.

outer:
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if i+j == 6 {
                break outer  // exits the outer loop entirely, not just the inner one
            }
            fmt.Printf("(%d,%d) ", i, j)
        }
    }
fmt.Println("\ndone")

goto

Go has goto but it is almost never used in practice. It jumps to a labeled statement within the same function. The only legitimate use case is breaking out of deeply nested code in generated code or low-level parsers. In normal application code, avoid it.

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

Practical Example — FizzBuzz

This example combines an expression-less switch with a for range loop. The switch acts as a clean replacement for the nested if/else chain that FizzBuzz typically requires.

package main

import "fmt"

func fizzBuzz(n int) string {
    switch {
    case n%15 == 0:
        return "FizzBuzz"
    case n%3 == 0:
        return "Fizz"
    case n%5 == 0:
        return "Buzz"
    default:
        return fmt.Sprintf("%d", n)
    }
}

func main() {
    for i := 1; i <= 20; i++ {
        fmt.Println(fizzBuzz(i))
    }
}

Output:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

Frequently Asked Questions

Does Go have a while loop?
No. Go only has one loop keyword: for. You use it as a while loop by omitting the init and post statements: for condition { }. You use it as an infinite loop with just: for { }.
Does switch in Go fall through by default?
No — the opposite of C. Each case in a Go switch breaks automatically. If you explicitly want to fall through to the next case, add the fallthrough keyword at the end of the case body.
Can I declare a variable inside an if statement?
Yes. Go allows an init statement before the condition: if err := doSomething(); err != nil { }. The variable is scoped to the if/else block only, which keeps error variables tightly scoped.