Skip to content
Codeloom
Go

Control Flow in Go: if, for, switch

Go's control flow in full — if with an init statement, the only loop (for) in all four forms, switch without break, type switches, and how defer fits in.

·9 min read · By Codeloom
Beginner 10 min read

What you'll learn

  • How Go's if works, including the init statement
  • Why Go has one loop keyword and how it covers four forms
  • Iterating with range over slices, maps, strings, and channels
  • switch without break, multi-case, and a preview of type switches
  • How defer schedules cleanup and when it runs

Prerequisites

Go has only three control-flow keywords you’ll use daily — if, for, and switch — plus defer, which schedules cleanup. The small surface area is deliberate: there is exactly one way to write a loop in Go, and the language designers wanted it that way. This post covers all of them in full.

if

The basic form is unremarkable:

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

Two rules to remember from Variables and Basic Types:

  • The condition must be a bool. Integers and strings can’t stand in for truthiness.
  • Braces are mandatory, even for one-line bodies.

if with an init statement

Go’s distinctive trick is letting you run a short statement before the condition, scoped to the if:

if err := doSomething(); err != nil {
    return err
}

err is declared, assigned, and tested in one expression. After the if/else ends, err goes out of scope. This is the pattern you’ll see most often around error handling — it keeps the error variable from leaking into the surrounding function.

You can also use it to compute a value once and reuse it:

if n := len(items); n > 100 {
    fmt.Printf("warning: %d items is a lot\n", n)
}

Comparisons

The standard operators: ==, !=, <, <=, >, >=. Boolean logic: &&, ||, !. Both && and || short-circuit, so this is safe:

if p != nil && p.Active {
    // p is dereferenced only if non-nil
}

for

for is the only loop in Go. It has four forms, each useful.

1. Three-clause for (the C-style)

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

The three clauses are init, condition, and post, separated by semicolons. The init declares variables scoped to the loop.

2. Condition-only for (Go’s while)

Omit the init and post clauses and you have a while loop:

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

There is no while keyword in Go. This is it.

3. Infinite for

Omit the condition entirely:

for {
    if shouldStop() {
        break
    }
    work()
}

Use break to leave the loop and continue to skip to the next iteration. This is the canonical way to write event loops and worker loops in Go.

4. for with range

range iterates over a collection, yielding index/value pairs. The exact pairs depend on what you iterate.

Over a slice or array:

words := []string{"go", "rust", "python"}
for i, w := range words {
    fmt.Printf("%d: %s\n", i, w)
}
// output:
// 0: go
// 1: rust
// 2: python

If you only need the value, discard the index with _:

for _, w := range words {
    fmt.Println(w)
}

If you only need the index, omit the value:

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

Over a map (covered in Slices and Maps):

ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
    fmt.Printf("%s is %d\n", name, age)
}

Map iteration order is deliberately randomised by Go. Never rely on the order.

Over a string, you get byte index and rune:

for i, r := range "héllo" {
    fmt.Printf("%d: %c\n", i, r)
}
// output:
// 0: h
// 1: é
// 3: l
// 4: l
// 5: o

The index jumps because é is two bytes in UTF-8.

Over a channel, range receives values until the channel is closed:

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

That’s all four forms. There is no do { ... } while, no Python-style for x in iter. Everything reduces to for.

break and continue

break exits the innermost loop:

for _, n := range nums {
    if n < 0 {
        break    // stop on first negative
    }
    fmt.Println(n)
}

continue skips to the next iteration:

for _, n := range nums {
    if n%2 != 0 {
        continue    // skip odd numbers
    }
    fmt.Println(n)
}

Go also has labelled break and continue for jumping out of nested loops:

outer:
for i := 0; i < 5; i++ {
    for j := 0; j < 5; j++ {
        if i*j > 6 {
            break outer
        }
    }
}

You’ll need labelled breaks rarely — once or twice in most codebases — but they’re cleaner than the alternatives when you do.

Try this. Write a small program that reads a list of integers from os.Args[1:], parses them with strconv.Atoi, and prints the sum of the even numbers only. Use a single for loop with continue to skip odd numbers. If strconv.Atoi returns an error for any argument, print the error and return — don’t silently skip bad input.

switch

switch in Go is more useful than in C-family languages because there is no implicit fall-through. Each case ends automatically — you don’t write break.

switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
    fmt.Println("weekday")
case "Sat", "Sun":
    fmt.Println("weekend")
default:
    fmt.Println("not a day")
}

A single case can match multiple values, separated by commas. The default runs if no case matches.

Switch on no value (clean if/else chains)

A switch without an expression is equivalent to switch true. The cases are arbitrary boolean expressions:

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

This is the idiomatic replacement for long if/else if/else chains.

Switch with an init statement

Like if, switch can run a short statement first:

switch n := rand.Intn(3); n {
case 0:
    fmt.Println("zero")
case 1:
    fmt.Println("one")
default:
    fmt.Println("other:", n)
}

fallthrough if you really want it

The keyword exists for the rare case where you want the next case to run anyway:

switch n {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two")
}
// when n == 1: prints both lines

Used very rarely. Most Go code never has a fallthrough in it.

Type switch (a preview)

A switch can also branch on the dynamic type of an interface value:

func describe(v interface{}) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %q\n", x)
    case []byte:
        fmt.Printf("bytes of length %d\n", len(x))
    default:
        fmt.Printf("unknown type %T\n", x)
    }
}

describe(42)
describe("hello")
// output:
// int: 42
// string: "hello"

We cover interfaces and type assertions properly later in the series. For now, file switch v.(type) as “the way to handle several possible types in one place.”

defer

defer schedules a function call to run when the surrounding function returns. We saw it briefly in Functions in Go; here are the rules in full.

A deferred call runs at function return, no matter how the function exits:

func work() {
    defer fmt.Println("cleanup")
    fmt.Println("doing work")
}
// output:
// doing work
// cleanup

Multiple defers run in LIFO order — last deferred, first executed:

func main() {
    defer fmt.Println("one")
    defer fmt.Println("two")
    defer fmt.Println("three")
    fmt.Println("body")
}
// output:
// body
// three
// two
// one

The arguments to a deferred call are evaluated immediately, but the call itself is delayed:

i := 1
defer fmt.Println("i was", i)    // "i was 1" — captures 1 right now
i = 99
// at return, prints: i was 1

This catches everyone at least once.

The classic use is paired-resource cleanup — file close, mutex unlock, HTTP response body close:

func readAll(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return io.ReadAll(f)
}

You acquire the resource, immediately defer its release, and stop thinking about cleanup for the rest of the function. This is one of Go’s nicest patterns.

Putting it together

A small program that demonstrates if with an init, a for range, a switch, and a defer:

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    defer fmt.Println("done")    // runs last

    if len(os.Args) < 2 {
        fmt.Println("usage: classify <word>...")
        return
    }

    for i, arg := range os.Args[1:] {
        word := strings.ToLower(arg)
        switch {
        case len(word) == 0:
            continue
        case strings.HasPrefix(word, "go"):
            fmt.Printf("%d: %q starts with go\n", i, word)
        case len(word) > 8:
            fmt.Printf("%d: %q is long\n", i, word)
        default:
            fmt.Printf("%d: %q is ordinary\n", i, word)
        }
    }
}

Run it:

go run . golang rust extraordinary hi
# output:
# 0: "golang" starts with go
# 1: "rust" is ordinary
# 2: "extraordinary" is long
# 3: "hi" is ordinary
# done

Every construct in this post is in there.

Try this. Modify the worked example so it also counts how many words start with each letter of the alphabet, and after the loop prints the counts in descending order. You’ll need a map (preview: see Slices and Maps in Go) and a sort — but you can write the counting half using only this post’s tools.

Recap

You now know:

  • if requires a bool condition and braces; an init statement can declare a variable scoped to the if.
  • for is Go’s only loop, in four forms: three-clause, condition-only, infinite, and range.
  • range yields index/value pairs over slices, key/value over maps, byte-index/rune over strings, and values over channels.
  • switch has no implicit fall-through; cases can list multiple values; switch { case cond: } replaces if/else chains; type switches branch on dynamic type.
  • defer schedules cleanup that runs at function return; arguments are evaluated when defer is executed; defers run in LIFO order.

Next steps

The next post covers Go’s two workhorse collection types — slices and maps — including the shared-backing-array gotcha that catches every Go programmer at least once.

→ Next: Slices and Maps in Go

Questions or feedback? Email codeloomdevv@gmail.com.