---
title: "07 — Control Flow — One Loop, Clear Conditionals"
uid: control-flow
tags: ["for", "switch", "golang", "break", "if", "roadmap:golang", "for-range", "loops", "fundamentals", "continue"]
excerpt: "One loop keyword (for, stretched to cover while and infinite loops), switch as a cleaner multi-branch if, mandatory braces, no parentheses. The minimalism is the point."
date: 2026-08-13T03:28:11+0000
source: https://www.aveshina.my.id/en/blog/control-flow
---

Every language before Go had given me a shelf of loop keywords, so Go's control flow felt smaller than expected — and that turned out to be the feature. The model that clicked: **Go has exactly one loop keyword, for, and it stretches to cover for, while, and infinite loops; conditionals are if and switch, both with mandatory braces and no required parentheses; and the language deliberately cuts goto to the margins.** [1] The minimalism is the point — one looping construct to learn, one formatting rule, no debates about braces. Once I stopped reaching for keywords that do not exist, the flow of Go code became very easy to scan.

## for — the only loop

Every loop in Go is a for. Three forms cover every case [1][2]:

```
// Classic C-style: init; condition; post
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style: condition only
n := 10
for n > 0 {
    n--
}

// Infinite: no condition
for {
    if done { break }
}
```

The classic form has the three familiar clauses — initialization, condition, post-statement — separated by semicolons. Drop the init and post, and you have a while. Drop the condition too, and you have an infinite loop that must be exited with break or return. There is no while keyword because for _is_ while [2]. Braces are _always_ required, even for a single-statement body, and the opening brace must sit on the same line as the for — the compiler inserts semicolons automatically and rejects a brace on the next line.

## for range — iterating collections

The companion form is for range, which walks arrays, slices, maps, strings, and channels [3]:

```
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("%d: %d\n", index, value)
}

// Strings yield runes, not bytes
for i, r := range "Go中" {
    fmt.Printf("%d: %c\n", i, r)   // 0: G, 1: o, 2: 中
}

// Maps yield key and value, in randomized order
for key, value := range someMap {
    fmt.Println(key, value)
}

// Ignore what you don't need with _
for _, value := range nums { /* only the value */ }
for index := range nums { /* only the index */ }
```

Three behaviors worth memorizing: over a string, for range decodes UTF-8 and yields runes (the index is the byte offset where each rune begins); over a map, the iteration order is randomized; over a channel, it yields only values and blocks until the channel is closed. The blank identifier _ is how you discard either the index/key or the value you do not care about [3].

A subtle rule: you cannot grow a slice during iteration safely (the range captures the original length), but you _can_ delete map entries mid-iteration. Adding map entries during iteration is unspecified and should be avoided.

## break, continue, and labels

break exits the innermost loop immediately; continue skips to the next iteration of the innermost loop [4]. Both work with labels when you need to control an _outer_ loop from inside an inner one:

```
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i*j > 3 { break outer }   // exits BOTH loops
        }
    }
```

Without the label, break would only escape the inner j loop. Labels are uncommon in clear code — usually a helper function with an early return reads better — but they exist for the cases where a nested search genuinely needs to bail out of everything at once.

goto exists in the language for completeness but is actively discouraged [5]. It can only jump to a label within the same function, and using it creates the unstructured spaghetti flow that every modern language tries to eliminate. The roadmap calls it out as discouraged, and in practice Go code simply does not use it — structured loops and early returns cover every legitimate need.

## if and if-else

Go's if is the conditional you know, minus the parentheses and plus mandatory braces [6]:

```
if score >= 90 {
    grade = "A"
} else if score >= 80 {
    grade = "B"
} else {
    grade = "C"
}
```

The one ergonomic feature is the **init statement** — a short declaration that runs before the condition and scopes the variable to the if/else blocks:

```
if user, err := lookup(id); err != nil {
    return err
} else {
    fmt.Println(user.Name)
}
// user and err are out of scope here
```

This is idiomatic for the "try something, check its error, branch on success" pattern, though the common Go style is to handle the error path first and keep the happy path un-nested. The condition itself must be a bool — there is no truthiness, so if 1 or if "x" are compile errors.

## switch — multi-branch without the noise

switch compares one value against several candidates and is cleaner than a chain of else if [7]. Two details differ from C-family switches, and both are improvements:

```
switch day {
case "Sat", "Sun":           // multiple values per case
    fmt.Println("weekend")
default:
    fmt.Println("weekday")
}
```

- **No automatic fallthrough.** Only the matched case runs; you do not need a break at the end of each case to stop it dripping into the next one. Fallthrough is opt-in via the explicit fallthrough keyword, which is rarely wanted [7].
- **Multiple values per case** with comma separation, as in case "Sat", "Sun":.

A switch with no expression is a cleaner way to write an if/else if chain — each case holds a boolean expression:

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

And the **type switch** (covered in the interfaces notes) is the idiomatic way to branch on the concrete type behind an interface value.

## How I use this

The single-loop minimalism reshaped how I write iteration. When I need to walk a collection, I reach for for range and use _ to drop the index I do not need — there is no separate forEach or iterator protocol to learn. For conditionals, I write switch the moment an if/else chain hits three branches, because the no-fallthrough default and multi-value cases make intent obvious. And the init-statement form of if is my default for any "call, check error, branch" sequence — it scopes the error variable to exactly where it is relevant and disappears after. The discipline of always-required braces and always-bool conditions removes a surprising amount of low-level noise from real code.

## References

[1] The Go Authors, "Go Tour: For loops," go.dev, 2024. [Online]. Available: [https://go.dev/tour/flowcontrol/1](https://go.dev/tour/flowcontrol/1)

[2] Nitesh, "Loops in GoLang: For Loop, While loop," Medium, 2024. [Online]. Available: [https://nitish08.medium.com/loops-in-golang-d44fb39b08e](https://nitish08.medium.com/loops-in-golang-d44fb39b08e)

[3] The Go Authors, "Range," Go Wiki, 2024. [Online]. Available: [https://go.dev/wiki/Range](https://go.dev/wiki/Range)

[4] DigitalOcean, "Using Break and Continue Statements When Working with Loops in Go," 2024. [Online]. Available: [https://www.digitalocean.com/community/tutorials/using-break-and-continue-statements-when-working-with-loops-in-go](https://www.digitalocean.com/community/tutorials/using-break-and-continue-statements-when-working-with-loops-in-go)

[5] The Go Authors, "Goto statements," Go Specification, 2024. [Online]. Available: [https://go.dev/ref/spec#Goto_statements](https://go.dev/ref/spec#Goto_statements)

[6] The Go Authors, "Go Tour: if and else," go.dev, 2024. [Online]. Available: [https://go.dev/tour/flowcontrol/7](https://go.dev/tour/flowcontrol/7)

[7] The Go Authors, "Switch," Go Wiki, 2024. [Online]. Available: [https://go.dev/wiki/Switch](https://go.dev/wiki/Switch)

```quiz
Q: How many looping constructs does Go have?
- One — `for`, which also covers while and infinite loops
- Three — for, while, and do-while
correct: 0
explain: Go has a single loop keyword, `for`. Drop the init/post to get a while loop; drop the condition too to get an infinite loop. There is no separate while keyword.

Q: What does a for-range loop over a string yield per iteration?
- the byte index and the byte value
- the byte offset where the rune starts, and the decoded rune
correct: 1
explain: for range over a string decodes UTF-8. The first value is the byte offset of the current rune (it jumps for multi-byte characters); the second is the rune itself.

Q: In a Go switch, what happens after a matching case runs?
- execution falls through to the next case unless you write break
- only the matched case runs; fallthrough is opt-in via the fallthrough keyword
correct: 1
explain: Go switched the default — cases do not fall through automatically, so no trailing break is needed. The rare case that wants fallthrough must request it explicitly.

Q: Which of these conditions is valid in a Go if statement?
- if 1 { ... }
- if count > 0 { ... }
correct: 1
explain: Go has no truthiness. The condition must be a bool. `if 1` is a compile error; you must write an explicit comparison like `if count > 0`.

Q: Why is goto discouraged in Go?
- It is not part of the language at all
- It creates unstructured control flow that is hard to read and debug, and structured loops plus early returns cover every legitimate need
correct: 1
explain: goto exists but is actively discouraged. Structured constructs (for, switch, early return from helpers) express the same intent more clearly, and real Go code essentially never uses goto.
```
