---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/go-fundamentals/control-flow'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Runs proof of which parts are optional in Go's for and switch forms"
    - 'Measures that all five forms fill an unwritten part with a defined behavior'
    - 'Shows that an unwritten break does not come free by proving that code wanting to fall through has to write an extra word'
---

# Control Flow

Five forms are measured with the same question — unconditional for, single-condition for, expressionless switch, an early return without an else, a switch branch that does not fall through on its own — and all five fill the unwritten part with a defined counterpart. Limiting measurement: an unwritten break is not needed, but code that wants to fall through has to write an extra word (fallthrough).

The previous lesson measured fifteen unwritten cases, but none of them sat **inside** a control
structure — all of them were about the absence of a variable, a return, or an argument. This
lesson carries the same question into control flow: why can `for` sometimes be written without
a condition, why can `switch` sometimes be written without an expression, and what stands in
for a `break` left unwritten at the end of a branch?

The Programming Foundations course (M01/K02) built conditional branching and loop control using
Python; there, loops and branching had separate keywords (`while`, `for`, `if`/`elif`/`else`)
and each had its own fixed syntax. **These concepts are not rebuilt here** — one sentence is
enough to state the difference: Go uses a **single** keyword for loops (`for`), and which
classic loop form that word corresponds to depends on which parts of the source are written and
which are left unwritten. This lesson measures that dependency.

## Five Forms, One Question

Go's control flow rests on two keywords: `for` and `switch` (`if` is a third, but the only
thing left unwritten there is `else`). The distinction other languages carry with separate
words — "infinite loop," "conditional loop," "counted loop" — is reduced in Go to **which parts
of a single `for`** are written. We run five separate forms through the same frame: each leaves
something unwritten, and the language fills that gap on its own.

```go
// flow.go — five forms, each filling an unwritten part with a defined counterpart
package main

import "fmt"

func classify(x int) string {
	if x < 0 {
		return "negative"
	}
	return "not negative"
}

func main() {
	counter := 0
	for {
		counter++
		if counter == 5 {
			break
		}
	}
	fmt.Printf("%-33s %s\n", "unconditional for (until stop)", fmt.Sprint("counter=", counter))

	i, total := 0, 0
	for i < 5 {
		total += i
		i++
	}
	fmt.Printf("%-33s %s\n", "single-condition for", fmt.Sprint("total=", total, " final_i=", i))

	score := 72
	var grade string
	switch {
	case score >= 90:
		grade = "AA"
	case score >= 70:
		grade = "BA"
	default:
		grade = "FF"
	}
	fmt.Printf("%-33s %s\n", "expressionless switch", fmt.Sprint("grade=", grade))

	fmt.Printf("%-33s %s\n", "early return without else", fmt.Sprint(classify(-3), " / ", classify(3)))

	result := ""
	switch 2 {
	case 1:
		result += "one"
	case 2:
		result += "two"
	case 3:
		result += "three"
	}
	fmt.Printf("%-33s %s\n", "switch does not fall through", fmt.Sprint("result=", result))
}
```

```
unconditional for (until stop)    counter=5
single-condition for              total=10 final_i=5
expressionless switch             grade=BA
early return without else         negative / not negative
switch does not fall through      result=two
```

Each of the five lines rests on a number that can be read independently of its own body.
`total=10` is the sum of `0+1+2+3+4`, confirming the loop ran five times (`i` from zero to
four) and kept going as long as the `i < 5` condition held; `final_i=5` shows the loop stopped
the instant that condition stopped holding. `grade=BA` proves the score of 72 fell into the
`score >= 70` branch and the `score >= 90` branch was never tried at all. All five are
independent proofs of one another, but every one answers the same question: what fills in the
part that was not written?

**LB17.** All three parts of `for` (initialization, condition, post-statement) are optional. In
the first line none of them is written; the compiler puts an **implicit `true`** in place of
the unwritten condition, which is why the loop does not stop on its own — `counter` shows it
would keep growing without bound until `break`, and we stop it at five ourselves. In the second
line, only the condition is written; when initialization and the post-statement are left
unwritten, the language skips them outright, and it is enough that `i` was defined before the
loop and incremented by hand inside the body.

Both `for` forms show the same claim from opposite ends: an unwritten part is met by the
language saying "consider this part absent" — with no condition it is always taken as true,
with no initialization or post-statement they are never run at all. Both belong to the same
`for` keyword; which parts are written decides which classic form (infinite, conditional,
three-step) the loop corresponds to.

This means `for` carries a single syntax tree, but all three branches of that tree are
optional. The three-step form (`for i := 0; i < 5; i++ { }`) skips no part and stands in its
most explicit shape in the source; the single-condition form in the second line moves
initialization and the post-statement outside, before and inside the body —
`i, total := 0, 0` before the loop, `i++` on the body's last line. For the compiler these two
forms produce the same machine code; the only difference is entirely what the reader sees at a
glance. In the three-step form, the loop's bounds gather into a single line; in the
single-condition form, seeing the bounds requires reading the whole body.

This distinction has a cost: in the first line, `counter` is reset before the loop and
increases inside it, but where the loop stops is written neither in a header nor on a single
line — it is hidden in an `if` block in the middle of the body. The unconditional `for` is the
most flexible form because it pins the stopping condition **nowhere**; that flexibility
requires reading the entire body to find the stopping condition.

This flexibility has a reason as much as a cost. Some loops have a stopping condition too
complex to express with a single comparison — it depends on more than one condition holding
together, or on a signal arriving from an outside source (a channel, a reader). For a loop like
this, writing a single condition in the header would mean trying to **squeeze** the body's real
stopping logic into the header; the unconditional `for` is the way to leave that stopping logic
in the body as is. The three-step and single-condition forms are preferred when the stopping
condition can be reduced to a single comparison; the unconditional form, when it cannot.

**LB18.** In the third line, `switch` is written without taking an expression. When a `switch`
expression is not written, the compiler compares it against an **implicit `true`**; each `case`
in turn becomes a logical expression of its own. Since `score >= 70` is true, `grade` becomes
`"BA"` — exactly what an `if`/`else if` chain would do, only with `switch` syntax.

This shows that `switch` in Go is less an "equality" construct and more a "branching" one. In
some languages, `switch` can only compare a single value against constant values; in Go, when
no expression is written, `case`s can carry free-form logical conditions, and those conditions
are tried in order, with the first true one running — exactly as in an `if`/`else if` chain.
There is no difference at all between writing `switch { }` and writing `switch true { }`; the
second only makes explicit the part the first leaves unwritten.

This shows that `switch` merges two separate uses into a single syntax. When an expression is
written (`switch score { case 90: ... }`), each `case` can carry a list of constants
**compared for equality** against that expression — multiple-value matching is one example of
this, and it will reappear later in the course. When no expression is written, each `case`
instead carries a complete logical expression of its own, and what is tested is not equality
but truth. The same keyword turns into two different branching forms depending on the single
part that is written — the expression itself.

In the fifth line, a different unwritten case is measured: no `case` ends with `break`, yet
`result` carries only the value of the matching branch (`case 2`) — `case 3`'s body never runs
at all. In Go, the end of a `case` means the branch **closes** on its own; what stands in for
the unwritten `break` is that falling through **never happens at all**.

The fourth line comes from a different family: with no `else` ever written, the `if` block ends
with a `return`, and the remaining code runs as though it were an `else` block.
`classify(-3)` enters the `if` branch and returns `"negative"`; `classify(3)` never enters the
`if` branch and falls through to the second line, returning `"not negative"`. What stands in
for the unwritten `else` is that every path leaving the `if` block — whether by a `return` or
by never entering at all — **arrives** at the same place, the rest of the function.

This form is a pattern known in Go sources as a "guard clause" — a way to filter out
exceptional cases early with a `return` at the top of a function, so that the rest of the body
can be written without one more level of nesting. Had an `else` followed the line
`if x < 0 { return "negative" }`, the rest of the function would have shifted one level deeper;
because no `else` is written, the second `return` stays at the same indentation level as the
function's main body. What stands in for the unwritten `else` is, for this reason, not only a
behavior but also a gain in **form**: the body reads as a flat sequence instead of nested
blocks.

This pattern becomes even more visible when a function filters out more than one exceptional
case. When several guard clauses are written one after another (`if a { return }`,
`if b { return }`, `if c { return }`), each writing its own `else` would push the body one
level deeper on every line; with no `else` ever written, all the guard clauses sit side by side
at the same indentation, and the function's "real work" stays at the end, at the least
indented line. The unwritten `else`, here, affects the readability not of a single line but of
the **whole** function.

## Limiting Measurement: An Unwritten Break, a Fallthrough That Must Be Written

The "no fall-through" behavior in the fifth line looks like a convenience — we were spared
writing one word (`break`). But when we want the reverse — a branch deliberately falling
through into the next one — the language does not give that **for free**.

The importance of this measurement lies in the answer it gives to the question "is an unwritten
thing always the shortest path." As we saw in the fifth line, no `case` wrote `break`, and yet
only the matching branch ran — that looked like a gain. But this gain does not mean "falling
through" never exists in the language; it only means it is **not the default**. The following
two `switch` statements carry the same three branches and test the same value (`2`); the only
difference between them is that the second adds one word to the `case 2` body.

```go
// limiter.go — an unwritten break costs nothing, but wanting to fall through costs a word (fallthrough)
package main

import "fmt"

func main() {
	notWritten := ""
	switch 2 {
	case 1:
		notWritten += "one"
	case 2:
		notWritten += "two"
	case 3:
		notWritten += "three"
	}

	written := ""
	switch 2 {
	case 1:
		written += "one"
	case 2:
		written += "two"
		fallthrough
	case 3:
		written += "three"
	}

	fmt.Printf("%-28s %s\n", "break not written", fmt.Sprint("result=", notWritten))
	fmt.Printf("%-28s %s\n", "fallthrough written", fmt.Sprint("result=", written))
}
```

```
break not written            result=two
fallthrough written          result=twothree
```

**LB19.** Both `switch` statements test the same value (`2`) and carry the same three
branches; the only difference is that the second `switch` adds the `fallthrough` word to its
`case 2` body. In the first line the result is only `"two"` — `case 2` runs, `case 3` is never
reached at all. In the second line the result is `"twothree"` — a deliberately requested
fall-through runs `case 3` right after `case 2`.

This adds an asymmetry to the question of "what stands in for the unwritten." The default
behavior (not falling through) needs no word at all; the **opposite** of the default (falling
through) needs a named, single-purpose keyword. This asymmetry is deliberate: someone reading
the body of a `case`, as long as they do not see `fallthrough` there, can be sure that branch
ends on its own. What is unwritten here is not a gap, it is a **guarantee** — only one reading
remains valid: the branch closes on its own unless stated otherwise.

`fallthrough` carries one more restriction: it can only fall into the **next** `case`, without
testing that case's own condition. In the second `switch`, `case 3`'s own condition (being
equal to `3`) is never tested at all — the moment `fallthrough` is seen, the next branch runs
unconditionally. This shows that `fallthrough` should not be confused with **retesting a
case's condition**: `fallthrough` does not mean "test the next branch's condition too," it
means "run the next branch's **body** too." This is a detail that needs to be told apart at
writing time — someone who does not know that `fallthrough` runs the next branch without
testing it might assume the falling-through branch's condition gets checked too.

`fallthrough` comes with a restriction of its own: it can only be the **last** statement in a
`case` body. Writing it in the middle of a branch would be rejected by the compiler — falling
through is a single transition that comes after the rest of the branch has run, not a branching
tool that can be scattered inside the branch. This restriction sets `fallthrough` apart from an
ordinary `goto`: it can only move to an **adjacent** next branch, and it can only appear at the
very end of a branch.

This asymmetry resembles, but sits in a different place from, the "rejecting does not mean
writing less" observation measured in the course's first lesson. There, the counterpart of a
rejected pattern was a longer piece of writing; here nothing is rejected, it is only that the
**non-default** path is written longer. The default behavior always takes the shortest
writing; the non-default behavior always asks for one more word.

Looking at all five forms together, a shared pattern emerges: in Go's control flow, every
unwritten part stands in for the **most commonly wanted** behavior. A loop running forever is
rarely wanted, so the unconditional `for` needs no special syntax — the mere absence of a
condition is enough. A `switch` branch falling into the next one is rarely wanted, so not
falling through is the default; wanting to fall through means this exceptional request must be
flagged on its own. Code continuing after an `if` block is commonly wanted, so it continues
even with no `else` written. What is left unwritten is always the **majority's** path; what
must be written is always the **minority's** exception.

It is also worth noting that all five forms can be run side by side in a single measurement
program: none affects another, none needs any special setup. This shows that control flow in
Go was designed as a set of small, independent rules — understanding one form does not require
knowing the others; each answers, on its own, a single unwritten case with a single answer.

## Summary

- All three parts of `for` are optional; an unwritten condition becomes an implicit `true`, an
  unwritten initialization and post-statement are skipped outright.
- An expressionless `switch` is compared against an implicit `true`, and each `case` becomes a
  logical expression of its own.
- When an `if` block ends with a `return`, the rest of the code runs like an `else` block even
  with no `else` written.
- A `switch` branch closes on its own with no need for an unwritten `break`; all five forms fill
  their gap with a defined counterpart.
- Wanting the opposite of the default (falling through) does not come free: the `fallthrough`
  word has to be written explicitly.

## Next Step

The "early return without else" form measured in this lesson showed cases where a function
ends **early**, but the function's return value itself was never questioned — `classify`
always returned a single string, and no one ever left that string unwritten. The next lesson
asks that: when a function returns more than one value, what happens if one of them is left
unwritten, and what value does a named return carry when it is left completely untouched?
