Day 057 — Goroutines & the go statement¶
Month 3 · Week 1 · ⬅ Day 056 · Day 058 ➡ · Journal index
🎯 Learning Objective¶
Launch concurrent work with the go statement and wait for it correctly with sync.WaitGroup, while understanding what a goroutine actually is.
📚 Topics¶
- The
gostatement, goroutines vs OS threads, the Go scheduler (M:N) sync.WaitGroup(Add/Done/Wait), the Go 1.22 loop-variable fix- Concurrency vs parallelism
📖 Reading / Sources¶
- Tour of Go — Goroutines
- Effective Go — Goroutines
-
sync.WaitGroupdocs - Go blog — Concurrency is not parallelism
📝 Notes¶
- A goroutine is a function running independently, multiplexed by the runtime onto a small pool of OS threads (M:N scheduling). It starts at ~2 KB of stack that grows/shrinks, so spawning thousands is cheap → [[goroutines]].
go f(x)evaluatesfandxnow, then runs the call concurrently and returns immediately. The launching goroutine does not wait.- The
maingoroutine returning ends the whole process — stray goroutines are killed mid-flight. You must explicitly wait → [[sync-waitgroup]]. sync.WaitGroupis a counter:Add(n)before launching,Done()(usuallydeferred) per goroutine,Wait()blocks until zero. AlwaysAddon the launching goroutine, never inside the new one (it races withWait).- Concurrency ≠ parallelism: concurrency is structuring independent work; parallelism is running it simultaneously on multiple cores.
GOMAXPROCScontrols the latter → [[concurrency-vs-parallelism]]. - Go 1.22 gives each loop iteration a fresh loop variable, killing the classic "all goroutines see the last
i" bug. Pre-1.22 you neededi := ior a parameter. - A
WaitGroupmust not be copied after first use — pass a*sync.WaitGroup.
💻 Code Examples¶
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // BEFORE the go statement
go func() {
defer wg.Done()
fmt.Printf("worker %d\n", i) // fresh i per iteration on Go 1.22+
}()
}
wg.Wait() // block until all three call Done
Full code:
examples/month-03/goroutines/main.go· Run:go run ./examples/month-03/goroutines
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Bounded Map over a worker pool, ordered results |
✅ | exercises/month-03/week-1/workerpool/ |
🐛 Mistakes Made¶
- Forgot
wg.Wait()and the program exited before any worker printed — silent. - First put
wg.Add(1)inside the goroutine;go vet/-raceflagged the race withWait.
❓ Open Questions¶
- When is
errgroup(golang.org/x/sync) worth it over a rawWaitGroup? (Answer: when goroutines return errors or need cancellation — but that's a third-party pkg.)
🧠 Active Recall (answer without looking)¶
-
Q: Why might a goroutine launched in
mainnever run?
A
When `main` returns the whole process exits; unwaited goroutines are killed. Use a `WaitGroup` (or channel) to wait. -
Q: Why must
wg.Addbe called beforego, not inside the goroutine?
A
`Add` inside the goroutine races with `wg.Wait()` in the launcher — `Wait` could observe a zero counter and return before the goroutine even registered.
🪶 Feynman Reflection¶
A goroutine is like handing a task to an assistant and walking off — you keep working, they work in parallel. A WaitGroup is the clipboard where you tally how many tasks are out (Add), each assistant crosses theirs off (Done), and you wait at the door until the tally is zero (Wait).
🕳️ Knowledge Gaps¶
- How the scheduler handles blocking syscalls (handing off P to another M) — revisit later.
✅ Summary¶
I can spawn concurrent work with go and join it deterministically with a WaitGroup, and I understand concurrency is structure, not necessarily parallel execution.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 058: how goroutines communicate — channels, unbuffered vs buffered.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): goroutines and WaitGroup (day 057)