Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36