A goroutine is a function executing concurrently with other goroutines in the same address space. Unlike OS threads, goroutines are multiplexed onto a small number of kernel threads by the Go runtime scheduler, making them extraordinarily cheap to create — a fresh goroutine starts with a stack of just a few kilobytes that grows and shrinks on demand. Spawning ten thousand goroutines is routine; spawning a million is feasible on modern hardware.
The go keyword is the only syntax required. Prefix any function or method call with go and it runs asynchronously. There is no thread pool to configure, no executor to manage, no future object returned. The goroutine runs, and when the function returns, it silently exits. This simplicity is deceptive — coordinating goroutines requires channels, WaitGroups, or the context package, and failing to do so correctly leads to goroutine leaks, data races, and subtle deadlocks.
Under the hood, Go uses a work-stealing scheduler built on the GMP model: G (goroutines), M (OS threads), and P (logical processors). Each P maintains a local run queue of goroutines. When a goroutine blocks on I/O or a channel operation, the scheduler parks it and runs another goroutine on the same thread — no context switch to the kernel. This is why Go can handle hundreds of thousands of concurrent connections with a fraction of the memory that thread-per-request models require.
Master go goroutines
Take the Go Programming course with hands-on lessons and challenges.
The go keyword launches each fetchData call as a concurrent goroutine. All three run simultaneously rather than sequentially. In production code, you would never rely on time.Sleep for synchronization — use sync.WaitGroup or channels.
WaitGroup tracks a count of active goroutines. Add(1) increments before each launch, Done() decrements when a goroutine finishes, and Wait() blocks until the count hits zero. The range-over-int syntax (Go 1.22+) replaces the classic for i := 0; i < 10; i++ loop.
Instead of spawning a goroutine per task (which can overwhelm resources), a fixed pool of workers reads from a shared jobs channel. Closing the channel signals workers to exit their range loop. This pattern bounds memory and CPU usage regardless of how many tasks arrive.
Since Go 1.22, each loop iteration gets its own copy of the loop variable, so closures capture the correct value. In earlier Go versions, all goroutines would share a single url variable and typically all print the last URL. This was one of the most common concurrency bugs in Go.
leakyProducer spawns a goroutine that blocks forever once the consumer stops reading. safeProducer uses context cancellation so the goroutine exits cleanly when the caller is done. runtime.NumGoroutine() is useful during testing and debugging to verify goroutines are being cleaned up.
errgroup from golang.org/x/sync combines WaitGroup semantics with error propagation and context cancellation. If any goroutine returns an error, the context is cancelled (signaling other goroutines to stop) and Wait() returns the first error. This is the production-grade alternative to bare WaitGroups when errors matter.
Launching goroutines in a loop without waiting for them to finish — the program exits (or the function returns) before goroutines complete their work, silently dropping results
Use sync.WaitGroup, errgroup, or channel-based signaling to ensure all goroutines complete before proceeding. Every goroutine you launch must have a corresponding synchronization point.
Passing a WaitGroup by value instead of by pointer — the copy never decrements the original counter, causing Wait() to block forever (deadlock)
Always pass *sync.WaitGroup as a pointer. The go vet tool catches this: run `go vet ./...` as part of your CI pipeline to detect WaitGroup copy errors.
Spawning an unbounded number of goroutines for incoming requests or tasks — under load, this exhausts memory and causes the process to crash with an out-of-memory error
Use a worker pool pattern with a fixed number of goroutines reading from a buffered channel, or use a semaphore (buffered channel of struct{}) to cap concurrency. errgroup.SetLimit() also works.
Creating goroutines that have no exit path — a goroutine blocked on a channel send or receive that will never complete stays alive for the lifetime of the process, leaking memory
Every goroutine must have a cancellation mechanism: context.Context, a done channel, or a channel that will eventually close. Test for leaks using runtime.NumGoroutine() in your test suite.
Goroutines are Go's fundamental concurrency primitive — lightweight, runtime-managed units of execution launched with the go keyword. The GMP scheduler multiplexes them onto OS threads efficiently, making it practical to run hundreds of thousands concurrently. Coordination happens through channels, sync.WaitGroup, and errgroup. The critical discipline is ensuring every goroutine has an exit path — typically context cancellation — so that goroutine leaks do not silently accumulate.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.