Every mechanism this domain covered individually — zero values, multiple returns, explicit errors, goroutines, channels, implicit interfaces — shows up together in almost any real Go code. This closing lesson walks through one small, realistic function, naming exactly which earlier lesson explains each piece, the same synthesis pattern this curriculum's other closing lessons use.
3 min read
type Result struct {
URL string
Body string
Err error
}
func fetchAll(urls []string, timeout time.Duration) []Result {
results := make([]Result, len(urls)) // lesson 10: a slice, pre-sized
var wg sync.WaitGroup
ch := make(chan Result, len(urls)) // lesson 8: buffered channel
for _, url := range urls {
wg.Add(1)
go func(u string) { // lesson 7: goroutine per URL
defer wg.Done()
client := http.Client{Timeout: timeout}
resp, err := client.Get(u) // lesson 3 & 6: multiple returns, explicit error
if err != nil {
ch <- Result{URL: u, Err: err}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ch <- Result{URL: u, Err: err}
return
}
ch <- Result{URL: u, Body: string(body)}
}(url)
}
wg.Wait()
close(ch)
for r := range ch {
results = append(results, r) // lesson 10: append, not sharing capacity issues here
}
return results
}Result struct, doing error handling's actual jobResult bundles a URL, its body, and an error field together — this is lesson 6's explicit-error philosophy applied to a concurrent context: since a goroutine can't directly return a value to its caller the way an ordinary function call can (lesson 7 established goroutines are fire-and-forget), the error has to travel back through the channel alongside the actual result, as one bundled value, rather than being handled with an immediate if err != nil the way a sequential call would.
sync.WaitGroup, a coordination primitive this domain's lessons set up forwg.Add(1) before launching each goroutine and wg.Done() (via defer, guaranteeing it always runs even if the goroutine panics) when one finishes, combined with wg.Wait() blocking until every goroutine has called Done(), is the standard idiom for "wait until N concurrent goroutines have all finished" — a coordination need that exists specifically because lesson 7 established goroutines don't automatically signal completion to anything on their own.
ch is buffered with capacity len(urls) specifically so every goroutine's send can succeed without blocking, even if nothing has started receiving from the channel yet — lesson 8 covered this exact distinction: an unbuffered channel here could deadlock if all goroutines tried to send before the main function started its range ch loop to receive them.
http.Client and io.Writer/io.Reader-shaped interfaces, used without fanfareresp.Body satisfies Go's standard io.Reader interface implicitly (lesson 5) — io.ReadAll works on it without resp.Body's type ever declaring any relationship to io.Reader at all, exactly the implicit-satisfaction mechanism lesson 5 covered, used here completely unremarkably, the way it's used constantly throughout Go's own standard library.
None of these pieces is individually complicated — a struct, a loop, a few standard-library calls — but the function as a whole is a genuine, realistic instance of Go's actual design philosophy from lesson 1: explicit over implicit (errors, struct fields, the defers), composition over clever abstraction, and concurrency primitives (goroutines, channels, WaitGroup) that compose together cleanly rather than requiring a framework to orchestrate them. Reading real Go code fluently means recognizing these dozen or so recurring idioms on sight — the same pattern-recognition payoff this curriculum's other capstone lessons have built toward in their own domains.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does the capstone's Result struct bundle an error field alongside the actual result?
2. What role does sync.WaitGroup play in the capstone example?
3. Why is the channel in the capstone buffered to exactly len(urls)?
Go