Go Concurrency Cheat Sheet for Channels Locks and Context
A one page Go concurrency cheat sheet: channel behavior, select rules, sync primitives, atomics, context, common patterns and test commands.

A reference to keep open while writing concurrent Go. It states rules and behavior; the linked articles carry the reasoning and the examples. Behavior shown here was checked on Go 1.25.6. For the bigger picture, start from the concurrency patterns hub.
Tested with Go 1.25.6
Channel operations by state
| Operation | nil channel | open, not full or empty | open, full (send) or empty (receive) | closed |
|---|---|---|---|---|
ch <- v | blocks forever | sends | blocks | panics |
<-ch | blocks forever | receives | blocks | returns buffered values, then zero value |
v, ok := <-ch | blocks forever | ok true | blocks | ok false once drained |
close(ch) | panics | closes | closes | panics |
len(ch), cap(ch) | 0, 0 | count, capacity | count, capacity | remaining count, capacity |
Rules of thumb: the sender closes, never the receiver; one closer per channel; a channel does not need closing unless a receiver ranges over it (closing channels). Direction types document intent: chan<- T is send-only, <-chan T is receive-only (channels).
for v := range ch { ... } // receives until closed and drained
v, ok := <-ch // ok == false means closed and empty
select { case ch <- v: default: } // non-blocking send: drop if fullselect
| Situation | Result |
|---|---|
| One case ready | that case runs |
| Several cases ready | one chosen uniformly at random |
None ready, has default | default runs |
None ready, no default | blocks until one is ready |
case <-nilChan | never chosen, so assigning nil disables a case |
select {} | blocks forever |
Priority needs two selects: a non-blocking check of the urgent channel first, then the combined one (select fairness).
sync primitives
| Need | Use | Notes |
|---|---|---|
| Protect shared state | sync.Mutex | Lock, then defer Unlock; never copy |
| Many readers, rare writers | sync.RWMutex | costs more than Mutex; no recursive read locks |
| Wait for goroutines | sync.WaitGroup | wg.Go(f) in Go 1.25; Add before go otherwise |
| Run once | sync.Once, OnceFunc, OnceValue | later callers see the first call's effects |
| Reuse temporary objects | sync.Pool | may be emptied by the GC; not a cache |
| Condition waiting | sync.Cond | prefer a channel; always Wait in a loop |
| Concurrent map, special cases | sync.Map | write-once read-many or disjoint keys |
| One counter, flag or pointer | sync/atomic types | atomic.Int64, atomic.Bool, atomic.Pointer[T] |
Details: mutex, RWMutex, WaitGroup, Once, Pool, Cond, atomics.
context
| Function | Purpose |
|---|---|
context.Background() | root context for main, tests and top-level work |
context.WithCancel(parent) | manual cancellation; always defer cancel() |
context.WithTimeout(parent, d) | cancels after a duration |
context.WithDeadline(parent, t) | cancels at a time |
context.WithCancelCause(parent) | cancel with a reason; read it with context.Cause |
context.WithoutCancel(parent) | keeps values, drops cancellation (for cleanup) |
context.AfterFunc(ctx, f) | runs f when ctx ends |
ctx.Err() | context.Canceled or context.DeadlineExceeded once done |
Conventions: first parameter, named ctx; never stored in a struct; values only for request-scoped data (context, cancellation).
Patterns, in one line each
| Pattern | Shape | Article |
|---|---|---|
| Wait for N tasks | WaitGroup, results in a slice by index | WaitGroup |
| First error cancels the rest | errgroup.WithContext | errgroup |
| Bound concurrency | semaphore channel, errgroup.SetLimit | bounded parallelism |
| Worker pool | N workers range over a jobs channel | worker pool |
| Fan-in | goroutine per input, closer after Wait | merging channels |
| Pipeline | stage functions, channel to channel | pipelines |
| Collapse duplicate calls | singleflight.Group.Do | singleflight |
| Rate limit | rate.Limiter | rate limiting |
| Timeout a blocking call | context.WithTimeout passed down | timeouts |
| Graceful shutdown | signal.NotifyContext, Server.Shutdown | shutdown |
Runtime errors and what they mean
| Message | Cause |
|---|---|
fatal error: all goroutines are asleep - deadlock! | every goroutine is blocked and none can proceed |
fatal error: concurrent map writes (or read and map write) | a map used from several goroutines without a lock; not recoverable |
panic: send on closed channel | a sender used a channel that was already closed |
panic: close of closed channel / close of nil channel | double close, or closing a channel that was never made |
panic: sync: negative WaitGroup counter | more Done calls than Add |
fatal error: sync: unlock of unlocked mutex | Unlock without a matching Lock; not recoverable |
context deadline exceeded / context canceled | the context ended; check with errors.Is |
The memory model in six lines
A go statement happens before the goroutine starts. A send happens before the matching receive completes. A close happens before a receive that returns because of it. On an unbuffered channel, the receive happens before the send completes. Unlock happens before the next Lock. Once.Do(f) finishing happens before any Do returns. Without one of these edges, two accesses to the same variable race (happens-before).
Rules worth memorizing
- Every goroutine needs a way to stop and an owner that waits for it.
- Every blocking send, receive or call should also watch cancellation or have a deadline.
- Shared memory needs an ordering edge: a lock, an atomic, a channel operation or
WaitGroup.Wait. - Do not hold a lock across I/O, logging or callbacks.
- Do not start goroutines in a loop without a bound.
- A panic in any goroutine ends the process unless that goroutine recovers.
Commands
go test -race ./... # data races in the executed paths
go test -race -count=20 -shuffle=on # vary the interleavings and test order
go test -cpu 1,2,8 # vary GOMAXPROCS
go vet ./... # copied locks, WaitGroup misuse, loop capture on old modules
GOTRACEBACK=all ./app # print every goroutine on a crash
kill -QUIT <pid> # dump all goroutines and exitPrograms expose profiles with net/http/pprof: /debug/pprof/goroutine?debug=2 for a full dump, and /debug/pprof/mutex and /debug/pprof/block once the rates are set (mutex and block profiles). The reference documents are the Go memory model, the sync package and the context package.


