> series: learn go from 0 to 100 | article 14 of 19
Channels are the preferred road, not the only one. Today we complete the toolbox with the sync package for classic synchronization and context, the standard mechanism for telling an army of goroutines the show is over. Getting Go context right is half of production readiness.
sync.WaitGroup: waiting without sleeping
The official replacement for those time.Sleep calls from previous articles:
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // +1 BEFORE launching
go func(id int) {
defer wg.Done() // -1 on exit, no matter what
process(id)
}(i)
}
wg.Wait() // blocks until the counter hits 0
fmt.Println("all done")
Three rules: Add goes BEFORE the go (inside, Wait might walk past before anyone adds), Done goes in a defer so it runs even on panic, and a WaitGroup must not be copied (pass a pointer if needed). Docs: pkg.go.dev/sync.
sync.Mutex: when sharing is unavoidable
Article 11 preached communicating over sharing. But for a counter or a small cache, a mutex is simpler and faster than staffing a data-owning goroutine:
type SafeCounter struct {
mu sync.Mutex
n int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // the Lock/defer Unlock duo
c.n++
}
There’s also sync.RWMutex (many readers, one writer), sync.Once for exactly-once initialization, and sync/atomic for atomic numeric operations. The compass: channels to orchestrate and pass data, mutexes to guard small state. And -race to confirm you haven’t fooled yourself.
context: cancellation with manners
Picture an HTTP request firing three queries, and the user closes the browser. Who tells those goroutines? That’s why context.Context exists (official blog):
// A 2 second timeout for the whole operation.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // ALWAYS release resources, even with time to spare
result := make(chan string)
go slowQuery(ctx, result)
select {
case r := <-result:
fmt.Println("in time:", r)
case <-ctx.Done(): // closes on cancel or expiry
fmt.Println("canceled:", ctx.Err()) // context deadline exceeded
}
A context forms a tree: you derive children with WithCancel, WithTimeout or WithDeadline, and canceling the parent cascades down to everything below. The signal travels through ctx.Done(), a channel that closes, and closing a channel wakes ALL its receivers at once. Elegant, right?

The context contract
The official convention for Go context, from the package docs: context travels as the FIRST parameter of any function crossing boundaries (network, disk, long jobs), named ctx. It’s not stored in structs, it’s passed. Here’s article 13‘s worker pool, now cancelable:package docs
// The respectful worker checks ctx.Done() in its loop:
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case <-ctx.Done():
return // party's over, clean up
case j, ok := <-jobs:
if !ok {
return
}
process(j)
}
}
}
And the piece that connects to the real world: every request in the stdlib HTTP server already carries its context in r.Context(), which auto-cancels if the client disconnects (net/http). Your only job is passing it down. We’ll use it hard in the project (articles 18 and 19).
About context.WithValue: it exists, it carries request-scoped data (trace IDs, authenticated user), and the docs ask for restraint. It’s not a global bag for parameters you’re too lazy to put in the signature.
Weekly challenge: put Go context to work
- The counter’s third life: fix article 11‘s data race with sync.Mutex and compare with article 12‘s channel version. Which reads clearer to you?
- Replace every waiting time.Sleep from your previous challenges with a WaitGroup.
- Cancelable worker pool: add context to article 13‘s challenge and cancel at 500 ms. Verify with -race and check no goroutines are left hanging.
- Bonus: race two simulated “APIs” with select and a WithTimeout: first to answer wins, and if neither makes it, report ctx.Err().
Go context: level complete.
Next article we switch gears: modules, go.mod, and how Go manages dependencies without 500 MB of node_modules.
> exit