Before this:Channels
select & synchronization
Key takeaways
select waits on several channels at once and acts on whichever is ready —
the key to handling multiple streams and cancellation. When channels aren’t the
right fit, the sync package provides a Mutex to guard shared state and a
WaitGroup to wait for goroutines to finish, and context carries
cancellation across a whole call tree.
Real concurrent programs juggle several channels and need a clean way to stop. This
lesson covers select and the coordination tools that round out Go’s concurrency
model.
select: waiting on many channels
select is like a switch, but its cases are channel operations. It blocks until
one is ready, then runs that case:
select {
case frame := <-frames:
process(frame) // data arrived
case <-quit:
return // asked to stop
}
This pattern — “handle data, but also watch for a stop signal” — is everywhere in
long-running services. Add a default case and select won’t block at all,
letting you poll a channel and move on if nothing’s waiting.
The sync package: when locks are simpler
Channels shine for moving data, but sometimes you just have shared state many
goroutines touch — a counter, a map, a cache. A sync.Mutex guards it:
var mu sync.Mutex
count := 0
func record() {
mu.Lock()
count++ // only one goroutine in here at a time
mu.Unlock()
}
Lock/Unlock ensure only one goroutine is in the critical section at once,
preventing the data race you’d otherwise get.
WaitGroup: waiting for goroutines to finish
If main returns while goroutines are still running, they’re cut off. A
sync.WaitGroup waits for them:
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c Channel) {
defer wg.Done()
decode(c)
}(ch)
}
wg.Wait() // blocks until every decode finishes
context: cancellation across a call tree
Long-running work needs a way to be told “stop now” — a user cancels, a timeout
fires. Go’s context carries that signal down through every function and
goroutine involved:
func run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return // cancelled or timed out
case s := <-samples:
process(s)
}
}
}
A single cancel() at the top propagates to every goroutine watching ctx.Done().
GopherTrunk uses exactly this shape to shut a scanner down cleanly.
Detecting mistakes
Concurrency bugs are subtle, so Go ships a race detector: run your tests with
go test -race and it flags any unsynchronized shared access at runtime. Make it a
habit — it catches the bugs that are hardest to find by reading.
Quick check: a goroutine must read incoming data but also stop when a quit channel fires. What does it use?
Recap
selectwaits on multiple channels and acts on whichever is ready — ideal for data-plus-cancellation.sync.Mutexguards shared state;sync.WaitGroupwaits for goroutines to finish.contextpropagates cancellation and timeouts across a whole call tree.go test -racecatches unsynchronized access — run it often.
Next up: how Go organizes code into packages and modules.
Frequently asked questions
What does the select statement do?
select waits on several channel operations at once and proceeds with whichever is ready first. It’s how a goroutine can, say, read from a data channel but also watch a cancellation channel — whichever fires first wins. With a default case, select can also try channels without blocking at all.
What is a WaitGroup for?
A sync.WaitGroup lets one goroutine wait for a group of others to finish. You call Add to count the goroutines, each one calls Done as it completes, and Wait blocks until the count reaches zero. It’s the standard way to make main wait for background work before the program exits.