Functions and control flow
Functions that give back more than one value, the single for loop that covers every repetition, switch without fall-through, and defer — Go's way of saying "when this function ends".
Functions return values, plural
A function writes its parameter types and its return type: func add(a int, b int) int. When two parameters share a type you may write it once — func add(a, b int) int — which is the common style.
Go functions can return more than one value, and the second one is usually an error. Even before the errors chapter, the shape is worth knowing, because you will see it in every package you import: value, err := doSomething(). The caller decides what to do with each.
One loop, three shapes
Go has a single loop keyword, for, and it covers everything: for i := 0; i < 10; i++ counts, for n < 10 repeats while a condition holds, and for alone repeats forever. range walks anything with items — for i, name := range names gives the position and the value; use _ for the one you do not need.
if takes a real boolean; there is no truthiness, so if count does not compile and if count != 0 is what you write. An if can also run a short statement before its condition — if err := save(); err != nil { … } — which keeps a value in exactly the block that uses it.
switch, and defer
switch is the cleaner else if chain: one value, a list of cases, and no fall-through — each case stops on its own unless you ask otherwise. A switch with no value at all takes conditions, and default catches the rest.
defer runs a call when the surrounding function returns, wherever it returns from. It is how Go closes files and unlocks locks: defer file.Close() sits next to the line that opened it, and it cannot be skipped by an early return. Deferred calls run in reverse order, the last scheduled first.
How a switch chooses
A switch evaluates its cases from top to bottom and runs the first that matches. That order is part of the meaning: put the narrow case above the broad one, or the broad one swallows it. In the snippet, n%15 == 0 has to come before n%3 == 0, because every multiple of fifteen is also a multiple of three. A switch with no value is a chain of conditions, and each case is a boolean expression. There is no fall-through to remember and no break to forget: the matched case ends, and only an explicit fallthrough continues into the next one.
The shapes side by side
package main
import "fmt"
func divide(a, b int) (int, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
func label(n int) string {
switch {
case n%15 == 0:
return "fizzbuzz"
case n%3 == 0:
return "fizz"
case n%5 == 0:
return "buzz"
default:
return fmt.Sprintf("%d", n)
}
}
func main() {
for n := 1; n <= 15; n++ {
fmt.Print(label(n), " ")
}
fmt.Println()
if result, ok := divide(10, 2); ok {
fmt.Println("10 / 2 =", result)
}
if _, ok := divide(1, 0); !ok {
fmt.Println("cannot divide by zero")
}
}Grades, one switch at a time
Run it, then add a case for scores below 50 that says "try again" — and change the function to also return the letter and the score, so main prints both.
The ok you see beside a second return value is a convention, not a keyword: a boolean that says whether the first value is trustworthy. It is the same shape as a lookup that might miss.
What happens when a function is called
A call makes a frame holding the parameters, the local variables and a list of deferred calls. Arguments arrive as copies, so changing a parameter inside the function changes the copy — unless the value is a pointer, or a slice or map whose contents are shared underneath.
When the function returns, its deferred calls run in reverse order, last scheduled first, and then the frame disappears. That is why defer is tied to the function and not to the block around it: a defer written inside a loop waits for the whole function, not for the end of that iteration.
Named results are ordinary variables owned by the frame. A bare return hands back whatever they hold, which is tidy in a three-line function and easy to misread in a thirty-line one. Prefer an explicit return with the values you mean.
Labels name a loop
A label names a loop so break and continue can say which one they mean. outer: before a for, then continue outer inside an inner loop, skips to the next turn of the outer loop rather than the inner one. Without the label, break leaves the innermost for, switch or select, and continue moves to the next turn of the innermost for. That is why a break written to leave a loop sometimes leaves only the switch inside it. If the intent needs a sentence to explain, the label is usually the sentence.
Deferring inside a loop is the classic leak: defer file.Close() over a thousand files keeps a thousand descriptors open until the function returns. Wrap the body in its own function, or close explicitly. Two more: if err := ... shadows an outer err, so the check afterwards inspects the old one; and break inside a switch leaves the switch, not the loop around it.
Try these next
- Rewrite the grade function with named results and a bare return, then decide which version you would rather review.
- Write a function that defers three prints, predict their order, and then run it.
- Add a labelled outer loop and use
continue outerfrom inside an inner loop.
A shipping quote with early returns
Run it, then add a surcharge for weights above 20 and a case that triggers it. Change the base cost and check every row still makes sense.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does _ mean in for _, name := range names?
