07 — Control Flow — One Loop, Clear Conditionals
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 hereThis 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
[2] Nitesh, "Loops in GoLang: For Loop, While loop," Medium, 2024. [Online]. Available: https://nitish08.medium.com/loops-in-golang-d44fb39b08e
[3] The Go Authors, "Range," Go Wiki, 2024. [Online]. Available: 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
[5] The Go Authors, "Goto statements," Go Specification, 2024. [Online]. Available: 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
[7] The Go Authors, "Switch," Go Wiki, 2024. [Online]. Available: https://go.dev/wiki/Switch
Knowledge check · Question 1 of 5
How many looping constructs does Go have?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!