Go Concurrency Patterns: Goroutines, Channels, and Beyond
Master Go's concurrency model — goroutines, channels, sync primitives, and production-ready patterns for building concurrent systems.
Go’s concurrency model is one of its greatest strengths. Goroutines are cheap, channels make communication clean, and the standard library gives you everything you need. But getting it right requires understanding the patterns. Let’s dig in.
Goroutines: Lightweight Threads
A goroutine is a function running concurrently with other goroutines in the same address space. They start with ~8KB of stack and grow as needed.
In other languages, 10,000 threads would exhaust memory. In Go, this is fine.
Channels: Communication Between Goroutines
“Do not communicate by sharing memory; instead, share memory by communicating.” — Go Proverb
Pattern 1: Worker Pool
Limit concurrency to avoid overwhelming downstream systems:
Pattern 2: Fan-Out, Fan-In
Distribute work across multiple goroutines, then merge results:
Pattern 3: Context for Cancellation
Always propagate context for cancellation and deadlines:
Pattern 4: errgroup for Parallel Operations
Run multiple operations concurrently and collect errors:
This runs all three queries in parallel, cutting 3 sequential queries (e.g., 3 × 50ms = 150ms) down to max(50ms, 50ms, 50ms) = ~50ms.
sync.Mutex vs sync.RWMutex
Use sync.RWMutex when reads are much more frequent than writes.
sync.Once for Initialization
Common Mistakes
Goroutine Leak
Always ensure goroutines can exit:
Data Race
Use go test -race to detect data races:
Closing a Channel Twice
Go’s concurrency model is powerful but requires discipline. Use -race in tests, always propagate context, and design goroutine lifecycles explicitly.