← Go, End to End

Go, End to End

Goroutines

A goroutine is a function running concurrently with whatever called it. This is the feature Go is most famous for, and the reason: goroutines are cheap enough that spawning thousands of them in one program is completely normal, where spawning thousands of OS threads would bring most systems to their knees.

The go keyword

Put `go` in front of any function call, and it runs as a new goroutine instead of blocking the caller. That's the entire syntax.

go · live, editable, runnable

time.Sleep as a synchronization mechanism is a code smell, shown here only because you haven't seen channels or WaitGroups yet. The moment main() returns, the whole program exits immediately, goroutines that haven't finished are simply abandoned mid-execution, no warning, no cleanup. The next two chapters cover the real ways to wait for goroutines properly.

Cheap, not free

A goroutine starts with a tiny stack (a few kilobytes) that grows and shrinks as needed, versus the megabyte-plus fixed stack an OS thread typically reserves. That's why "spawn a goroutine per incoming request" is a completely normal pattern in Go web servers handling thousands of concurrent connections, where the equivalent "a thread per request" design in other languages would exhaust memory or OS thread limits far sooner.

The scheduler, conceptually

Go's runtime multiplexes many goroutines onto a much smaller number of OS threads (roughly one per CPU core by default, tunable via `GOMAXPROCS`). When a goroutine blocks on something like a channel operation or I/O, the runtime parks it and lets another goroutine run on that OS thread instead, without you writing any of that scheduling logic yourself. This is often called M:N scheduling (M goroutines mapped onto N OS threads), and the exact mechanics are an implementation detail you don't need to memorize, but the conceptual model, many lightweight goroutines sharing a small pool of real OS threads, explains why concurrency in Go feels so cheap compared to threads.

Goroutines (thousands, cheap)G1 G2 G3 G4 G5 G6 ...
↓ scheduled onto ↓
OS Threads (few, expensive)M1 M2 M3 M4

Goroutine leaks: a real, concrete bug

A goroutine that blocks forever, waiting on a channel that will never receive a value or never get closed, never gets garbage collected. It just sits there, permanently, using its stack's worth of memory, for the rest of the program's life. Do this in a request handler that runs thousands of times a day, and you have a slow, silent memory leak that's brutal to diagnose in production.

go · live, editable, runnable

Nothing in this program crashes, nothing even prints a warning, that's exactly what makes goroutine leaks dangerous. The fix is always to make sure every goroutine has a real, guaranteed way to finish: a value it's certain to eventually receive, a channel that will definitely get closed, or a `context.Context` it's watching for cancellation (covered later in this course).

A quick mental checklist before writing `go someFunc()`

  • Does this goroutine have a guaranteed way to eventually stop, under every code path, including error cases?
  • Does the caller need to know when it's done, or care about its result? If so, you need a channel or WaitGroup to actually wait, not just a hope that it finishes in time.
  • Could this spawn an unbounded number of goroutines under load (one per incoming request, with no limit)? If so, you probably want a worker pool instead, covered in the concurrency patterns chapter.
Try it yourself: fix the leak with a buffered channel

The simplest fix for this specific leak: make the channel buffered with capacity 1, so the send can complete even with nobody listening, letting the goroutine finish and exit cleanly instead of blocking forever:

go · live, editable, runnable