Skip to content

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 go statement, 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

📝 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) evaluates f and x now, then runs the call concurrently and returns immediately. The launching goroutine does not wait.
  • The main goroutine returning ends the whole process — stray goroutines are killed mid-flight. You must explicitly wait → [[sync-waitgroup]].
  • sync.WaitGroup is a counter: Add(n) before launching, Done() (usually deferred) per goroutine, Wait() blocks until zero. Always Add on the launching goroutine, never inside the new one (it races with Wait).
  • Concurrency ≠ parallelism: concurrency is structuring independent work; parallelism is running it simultaneously on multiple cores. GOMAXPROCS controls 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 needed i := i or a parameter.
  • A WaitGroup must 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/-race flagged the race with Wait.

❓ Open Questions

  • When is errgroup (golang.org/x/sync) worth it over a raw WaitGroup? (Answer: when goroutines return errors or need cancellation — but that's a third-party pkg.)

🧠 Active Recall (answer without looking)

  1. Q: Why might a goroutine launched in main never run?

    A When `main` returns the whole process exits; unwaited goroutines are killed. Use a `WaitGroup` (or channel) to wait.

  2. Q: Why must wg.Add be called before go, 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)