Introduction
Go achieves massive concurrency not by mapping one goroutine to one OS thread, but by multiplexing many goroutines onto a small set of threads. Understanding this distinction is fundamental to writing efficient concurrent Go code.
Key Concepts
- Goroutine: A lightweight unit of execution managed by the Go runtime, starting with a ~2 KB stack that grows dynamically.
- OS Thread: A kernel-level thread with a fixed ~1 MB stack, expensive to create and context-switch.
- M:N Scheduling: Go maps M goroutines onto N OS threads, letting thousands of goroutines share a handful of threads.
Real World Context
In a production HTTP server, every incoming request typically spawns a goroutine. Because goroutines are so cheap, Go servers routinely handle tens of thousands of concurrent connections without the memory overhead that would cripple a thread-per-request model in languages like Java or C++.
Deep Dive
Go does not use one OS thread per goroutine. Instead, it uses an M:N scheduler (M goroutines on N OS threads).
- Lightweight: Goroutines start with a 2 KB stack (vs. 1 MB+ for threads).
- Dynamic: Stacks grow and shrink as needed via contiguous stack copying.
- Cooperative: The runtime switches contexts during blocking operations (I/O, channel sends/receives, sleep) or tight loops (via async preemption since Go 1.14).
Starting a goroutine is as simple as placing go before a function call:
gogo doWork() go func() { // Anonymous goroutine }()
Warning: The main function is itself a goroutine. When it exits, the program terminates, killing all other goroutines immediately without cleanup.
Common Pitfalls
- Forgetting main exits immediately — If main returns before background goroutines finish, their work is lost. Always use
sync.WaitGroupor channels to synchronize. - Assuming goroutines run in order — The scheduler is non-deterministic. Never rely on goroutine execution order without explicit synchronization.
Best Practices
- Always have a completion signal — Use a
WaitGroup, channel, orcontextso main knows when goroutines finish. - Keep goroutines short-lived — Long-running goroutines increase the risk of leaks. Prefer goroutines that do one job and exit.
Summary
- Goroutines are lightweight (~2 KB stack) compared to OS threads (~1 MB).
- Go uses M:N scheduling to multiplex goroutines onto threads.
- The
gokeyword launches a goroutine, but main must wait for them to finish. - Async preemption (since Go 1.14) prevents tight loops from starving the scheduler.
Code Examples
func main() {
go fmt.Println("In background")
fmt.Println("In main")
// Without a sleep or sync, main might exit before background runs
time.Sleep(100 * time.Millisecond)
}