Skip to content
academia.sh

Lesson 06 / 18

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).

Published

In this lesson

  • 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
Contents

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.

// 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, cases 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.

// 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?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close