Lesson 18 · Concurrency & context
Concurrency patterns
The primitives composed — worker loops, errgroup, graceful shutdown.
Your win: recognise the production patterns built from goroutines + channels + context + sync — and narrate our Kafka engine's graceful shutdown, which combines several at once.
Pattern 1 · The cancellable worker loop
A long-lived goroutine loops, doing work and selecting on
ctx.Done() so it can stop — with backoff on error so it doesn't hammer a
failing dependency:
for {
err := doWork()
if err != nil {
select {
case <-ctx.Done(): return // cancellable
case <-time.After(backoff(n)): continue // exponential backoff
}
}
}
That's runConsumerWithoutSelfEnd (golibs/kafka/kafka.go:672-771), which even rebuilds its reader against a reshuffled broker list on failure.
Pattern 2 · errgroup — run several, fail fast
errgroup.Group runs goroutines and gives you the first
error, cancelling the shared context so the others stop too:
errGr, ctx := errgroup.WithContext(ctx)
errGr.Go(func() error { return serveGRPC(ctx) })
errGr.Go(func() error { return serveHTTP(ctx) })
if err := errGr.Wait(); err != nil { ... } // first failure stops both
Pattern 3 · Graceful shutdown
Catch the OS signal, cancel the root context, wait for goroutines, then close resources. Our bootstrap wires the signal:
ctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) // bootstrap.go
Close() (kafka.go:788-838)
is the graceful-shutdown pattern end-to-end: (1) stop the health-check
goroutine (close its channel, Wait its WaitGroup); (2)
cancel each consumer's context.CancelFunc — the worker loops see
Done() and exit; (3) consumerWaitGroup.Wait()
until every goroutine has returned; (4) close the readers. Cancel →
Wait → close. No leaks, no half-finished commit.
Pattern 4 · Fan-out / fan-in & worker pools
Fan-out: launch N goroutines over the same input. Fan-in: merge their results into
one channel (Lesson 15's errChan + closer). A worker pool
bounds concurrency — a fixed number of goroutines range over a jobs channel
(conversationmgmt/.../http/util/worker_pool.go), so you
process many items without spawning unbounded goroutines.
panic in a goroutine with no recover crashes the
entire process — it can't be caught by whoever launched it. Long-lived
workers wrap their body in defer func(){ recover() }(); our
task_runner even respawns a worker that panicked
(task_runner.go:161-172). Know this: it's a favourite
"what happens if a goroutine panics?" question.
The Go Blog — "Pipelines and cancellation" + Concurrency Patterns
How to compose channels into pipelines and shut them down cleanly. Pair with the
errgroup docs.
Check yourself (from memory)
Q1. errgroup's Wait() returns…
Q2. Graceful shutdown in our services begins with…
Q3. A panic in a goroutine with no recover…
recover.
Close(): (1) stop the health-check goroutine (close
its stop channel, Wait its WaitGroup); (2) cancel each consumer's context
via its CancelFunc — the worker loops select on
Done() and return; (3) consumerWaitGroup.Wait() until every
consumer goroutine has exited; (4) close the readers. Signal-driven at the top by
signal.NotifyContext(SIGTERM, SIGINT). Cancel → Wait → close — no leaks.1. Go Concurrency Patterns: Pipelines and cancellation; errgroup.