Everything that matters about Go concurrency on one page. Each row links to the full topic; skim to refresh, then dive in.
🎯 The two proverbs
“Don’t communicate by sharing memory; share memory by communicating.” And “concurrency is not parallelism” — concurrency is how you structure independent work; parallelism is it actually running at once. Almost everything below follows from taking those seriously. See Concurrency vs Parallelism.
Channel axioms — what each operation does
| Operation | nil channel | open, ready | open, not ready | closed |
|---|---|---|---|---|
receive <-ch | block forever | returns a value | block | zero value, ok=false, now |
send ch <- | block forever | sends | block | panic |
close close(ch) | panic | closes | closes | panic |
Rules of thumb: the sender owns and closes the channel; closing signals “no more values”; a for range ch drains until close. Buffered channels decouple sender and receiver up to the buffer size. See Channels.
select — pick a ready case
| Idiom | Shape |
|---|---|
| Timeout | select { case v := <-ch: …; case <-time.After(d): … } |
| Non-blocking | select { case v := <-ch: …; default: … } |
| Cancellation | select { case <-ctx.Done(): return ctx.Err(); case … } |
| Disable a case | set its channel variable to nil (blocks forever) |
A select blocks until one case is ready; if several are, it picks one at random (prevents starvation). See select.
The sync toolbox
| Tool | Use it for |
|---|---|
Mutex | exclusive access to shared state |
RWMutex | many readers, occasional writer |
WaitGroup | wait for N goroutines to finish |
Once | exactly-once initialization |
Cond | wait/signal on a condition (rare) |
Pool | reuse short-lived allocations |
atomic | lock-free counters and flags |
Composable patterns
| Pattern | Use it for |
|---|---|
| Pipeline | stream data through channel-connected stages |
| Fan-out / Fan-in | parallelize a stage, then merge results |
| Worker Pool | bound concurrency with N workers |
| Semaphore | cap how many run at once (buffered chan struct{}) |
| Context & Cancellation | propagate cancel, deadline, request values |
| errgroup | concurrent tasks: wait + first error + cancel |
| Or-done | range a stream that stops cleanly on cancel |
| Pub/Sub | broadcast to dynamic subscribers |
Decision shortcuts — “I need to…”
✅ Channel or mutex, and which tool
- …transfer ownership of data between goroutines → channel
- …guard a struct’s internal fields →
Mutex - …wait for a group to finish →
WaitGroup - …run concurrent tasks that can fail → errgroup
- …cap how many run at once → Semaphore / Worker Pool
- …cancel a whole tree of goroutines →
context - …init exactly once →
sync.Once - …a fast counter without a lock →
atomic
Gotchas that bite
⚠️ Keep these in muscle memory
- Send on a closed channel panics; closing twice panics; receive on closed returns the zero value immediately.
- A nil channel blocks forever — handy to disable a
selectcase, surprising otherwise. - Every goroutine needs an exit — one blocked forever is a leak; give it a
done/ctx. WaitGroup.Addbefore you launch the goroutine, andDonein adefer.- A plain
mapisn’t safe for concurrent writes — guard it or usesync.Map; run the race detector with-race. - Don’t copy a
Mutex/WaitGroup— pass the struct by pointer. - Loop-variable capture in
go func(){…}was fixed in Go 1.22; on older versions, pass it as an argument.
Related topics
Concurrency is structure (independent activities); parallelism is simultaneous execution. CSP, GOMAXPROCS, channels vs mutexes, and Amdahl's law.
building-blocksChannelsTyped conduits that synchronize goroutines — direction, buffering, ownership, closing, and the axioms table that explains every behavior.
building-blocksThe sync PackageMutex, RWMutex, WaitGroup, Once, Cond and Pool — the lower-level primitives for guarding shared state, plus the copylocks rule and mutex-vs-channel guidance.
Check your understanding
Score: 0 / 31. What happens when you send on a closed channel?
Sending on a closed channel panics; so does closing it twice. Receiving from a closed channel is fine — it yields the zero value with ok=false, immediately.
2. You're transferring ownership of data from one goroutine to another. Channel or mutex?
Channels are for transferring ownership and coordinating; mutexes are for guarding a struct's internal state. Pick by intent, not by speed.
3. What does a receive on a nil channel do?
Send and receive on a nil channel both block forever. Setting a channel variable to nil to take its select case out of play is a real, idiomatic technique.
Comments
Sign in with GitHub to join the discussion.