Do you have multiple processing steps? YES → Pipeline └─ Is one step the bottleneck? YES → Fan-Out/Fan-In that stage NO → Keep it sequentialDo you have a batch of identical jobs? YES → How many goroutines? ├─ Fixed, reusable → Worker Pool ├─ One per job, bounded → Semaphore └─ One per job, throttled → Rate LimiterDo you need to limit throughput (not just concurrency)? YES → Rate Limiter (steady) or Token Bucket (burst + steady)Do you need to stop goroutines? YES → context.WithCancel / WithTimeout └─ Need to wait until they've finished? → WaitGroup or errgroupDo you need ordered results from parallel work? YES → Index-wrap results + sort (batch) or reorder buffer (streaming)
Channels: Unbuffered vs Buffered
Use
Channel Type
Why
Pipeline stages
Unbuffered
Backpressure flows upstream naturally
Fan-in / fan-out
Unbuffered
Synchronization is the point
Quit signal
Unbuffered
You want confirmation
Semaphore
Buffered (n)
Buffer size IS the concurrency limit
Rate limiter / token bucket
Buffered
Tokens accumulate up to a cap
Ring buffer
Buffered
Needs buffer to hold items before dropping
Default to unbuffered. Add a buffer only for bounding, throttling, or decoupling.
Concurrency Control Comparison
Worker Pool
Fan-Out/Fan-In
Semaphore
Goroutines
Few, long-lived
Few, long-lived
Many, short-lived
Output
Shared channel
Per-worker channels, merged
Each handles its own
Composability
Standalone
Chains into pipelines
Standalone
Best for
Job processing
Pipeline parallelism
Bounding parallel I/O
sync Primitives Quick Reference
Primitive
Purpose
Use When
sync.Mutex
Exclusive lock
Multiple fields updated together
sync.RWMutex
Read-many, write-exclusive
Read-heavy shared state
sync.WaitGroup
Wait for goroutines to finish
Coordinating goroutine completion
sync.Once
Run something exactly once
Lazy initialization
sync.Pool
Reuse temporary objects
Reducing GC pressure on hot paths
sync.Map
Concurrent map (lock-free reads)
Stable keys, read-heavy, profiling says so
sync.Cond
Condition variable
Waiting on shared condition (prefer channels)
sync/atomic
Lock-free single values
Counters, flags, simple state
Error Handling in Concurrent Code
Approach
Use When
Error channel
Simple fan-out, collect errors manually
errgroup
Fan-out with automatic WaitGroup + first error
errgroup.WithContext
Same + cancel remaining goroutines on first error
Panic recovery (safeGo)
Long-running services where one goroutine shouldn't crash everything
Common Pitfalls
Pitfall
Fix
Goroutine leak (blocked on channel)
Always use context for cancellation
Data race on shared state
Mutex, atomic, or communicate via channels
Deadlock from nested locks
Always acquire locks in the same order
Sending on closed channel (panic)
Only the sender closes; use sync.Once if multiple senders
Forgetting wg.Add before go
Always Add before launching the goroutine
time.Sleep for synchronization
Use channels, WaitGroup, or context instead
Key Takeaways
Start simple. A for loop is often enough. Reach for concurrency patterns when the problem genuinely calls for it — not because you can.
Channels for communication, mutexes for state. Don't force channels where a mutex is clearer. Don't share memory where a channel is simpler.
Default to unbuffered channels. They synchronize for free. Add a buffer only when you have a specific reason.
Always manage goroutine lifetime. Every goroutine you start must have a way to stop — context, done channel, or closed input channel. Leaked goroutines are silent memory leaks.
Pick the simplest pattern that fits. Worker pool before fan-out/fan-in. Semaphore before worker pool. Plain loop before any of them.
Concurrency is about structure, not speed. A well-structured concurrent program is easier to reason about, test, and extend — even if it's not faster.
Profile before optimizing. Don't reach for sync.Map, sync.Pool, or atomic operations until profiling tells you to. Correctness first, performance second.
The race detector is your friend. Run go test -race and go run -race during development. It catches bugs you won't find by reading code.
📝 Ready to test your knowledge?
Answer the quiz below to mark this lesson complete.