Go Mastery 3: Concurrency Done Right
/ 3 min read
Table of Contents
1. Goroutines
-
go f(): cheap, multiplexed onto OS threads - Goroutine lifecycle: every goroutine needs a way to stop
- Goroutine leaks and how to find them
- The scheduler (G-M-P model): conceptual understanding
-
GOMAXPROCS
Visual: the G-M-P scheduler
flowchart TB
subgraph P1["P1 (logical processor)"]
Q1["Local run queue: G1, G2, G3"]
end
subgraph P2["P2 (logical processor)"]
Q2["Local run queue: G4"]
end
GQ["Global run queue"]
P1 --> M1["M1 (OS thread)"]
P2 --> M2["M2 (OS thread)"]
GQ -.-> P1
GQ -.-> P2
Q1 -. "work stealing" .-> Q2
G = goroutine, M = OS thread, P = processor token. Idle Ps steal work from busy ones.
2. Channels
- Unbuffered vs buffered
- Send, receive,
close, receiving from closed channel,v, ok := <-ch - Only the sender closes; closing twice panics
- Nil channels block forever (useful in
select) - Directional channels:
chan<-,<-chan -
for v := range ch
3. select
- Multiplexing,
defaultcase, timeouts withtime.After - Combining with
context.Done() - Non-blocking send/receive
4. sync package
-
sync.WaitGroup(andwg.Goin newer Go versions) -
sync.Mutex,sync.RWMutex -
sync.Once,sync.OnceFunc,sync.OnceValue -
sync.Pool -
sync.Map(and when not to use it) -
sync.Cond(rare) -
sync/atomicand typed atomics (atomic.Int64) -
golang.org/x/sync/errgroupandsemaphore
5. context package
-
context.Background,WithCancel,WithTimeout,WithDeadline,WithValue - Context is the first parameter:
func F(ctx context.Context, ...) - Never store context in structs
- Propagate cancellation through every layer
-
WithValuefor request-scoped data only, never optional params
Visual: context cancellation tree
flowchart TD R["context.Background()"] --> A["WithCancel: request ctx"] A --> B["WithTimeout: DB query"] A --> C["WithTimeout: external API call"] A --> D["Worker goroutines"] A -. "cancel()" .-> X["All children receive ctx.Done()"]
6. Patterns
- Worker pool
- Fan-out / fan-in
- Pipeline
- Producer-consumer
- Rate limiting (
time.Ticker,x/time/rate) - Pub/sub
- Graceful shutdown
- Bounded parallelism
Visual: worker pool and fan-in
flowchart LR
P["Producer"] --> J[("jobs channel")]
J --> W1["Worker 1"]
J --> W2["Worker 2"]
J --> W3["Worker 3"]
W1 --> R[("results channel")]
W2 --> R
W3 --> R
R --> C["Collector"]
Close jobs when the producer is done, wait on a WaitGroup, then close results.
7. Bugs you must be able to spot
- Data races (use
go test -racealways) - Deadlocks (
all goroutines are asleep) - Goroutine leaks (blocked forever on a channel)
- Copying a mutex
- Closure capturing a loop variable (pre-1.22)
- Forgetting
wg.Addbefore launching the goroutine
8. Philosophy
“Don’t communicate by sharing memory; share memory by communicating.”
But in practice: use a mutex for protecting simple state, channels for passing ownership and coordinating work.
9. The Go memory model
- Read https://go.dev/ref/mem
- Understand happens-before, and why synchronization is required
Practice
- Build a concurrent web crawler with a depth limit and a worker pool
- Implement a rate limiter using channels
- Build a pipeline: read file -> parse -> transform -> write, with cancellation
- Write a graceful-shutdown HTTP server
- Intentionally create a race, then catch it with
-race
Previous: Go Mastery 2: Interfaces, Errors, Generics, Modules
Next: Go Mastery 4: Standard Library and Tooling