Part II — Intermediate: concurrency and the standard library
Part I gave you the language running on a single thread. This part is where
Go actually distinguishes itself: goroutines, channels, and the sync
primitives that make "just spin up a thousand of these" a reasonable
sentence, plus the standard-library tools (testing, generics, net/http)
you'll reach for daily. It closes with two real concurrent tools, not toy
examples — the worker pool and LRU cache in Chapters 25–26 are worth
building yourself, not just reading.
15. Goroutines: Go's answer to "what is a thread"
15.1 Goroutines are not OS threads
A goroutine starts at ~2KB of stack (growable, shrinkable, managed by the Go runtime) versus a typical OS thread's 1–8MB fixed stack. The Go runtime multiplexes many goroutines onto a small number of OS threads (the GMP model, Chapter 28) — which is why spawning 100,000 goroutines is routine in Go, while 100,000 OS threads would exhaust most systems.
go doSomething() // fire-and-forget — but you own its lifecycle now
go func() {
doSomethingElse()
}()
15.2 The goroutine leak
Starting a goroutine is one line. Making sure it exits is the actual engineering problem. A goroutine blocked forever on a channel receive that nothing will ever send to is a leak — it holds its stack and whatever it closed over for the life of the process:
func leaky() {
ch := make(chan int) // unbuffered, no sender registered
go func() {
v := <-ch // blocks forever — ch is never written to or closed
fmt.Println(v)
}()
// leaky() returns; the goroutine above never will
}
The fix is usually the same shape: give long-lived goroutines an explicit
way to be told to stop (a context.Context, a done channel) and know
what event ends each goroutine you start.
15.3 runtime.NumGoroutine() as a diagnostic
fmt.Println("active goroutines:", runtime.NumGoroutine())
In production, a steadily climbing goroutine count exposed on a /debug
metric is usually the first visible symptom of a leak, well before memory
pressure becomes an incident.
15.4 War story
A payments service exposed runtime.NumGoroutine() as a Prometheus
gauge "just in case." Eighteen months later, the number had been slowly
climbing for weeks — invisible in latency or error-rate dashboards — and
the on-call engineer only caught it because the gauge crossed an
arbitrary alert threshold. The root cause: an HTTP client call without a
timeout, hitting a downstream service that had started hanging instead of
erroring, leaving one goroutine parked per stuck request, forever. The
fix was a context.WithTimeout on every outbound call (Chapter 18) — a
one-line change per call site, but only findable because the metric
existed at all. Lesson: goroutine count is a cheap, high-signal metric
almost nobody adds until after the first leak incident.
Applied in the series: Linux guide, Chapter 73 measures what goroutines cost in OS threads: 20,000 waiting goroutines used 16 threads, while 100 blocked in raw syscalls pushed it to 108.