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:

internal/golibs/bootstrap/bootstrap.go:321-359
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
Anchor — narrate this in an interview The Kafka engine's 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.

Pattern 5 · Recover in spawned goroutines A 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.
Read this next

The Go Blog — "Pipelines and cancellation" + Concurrency Patterns

How to compose channels into pipelines and shut them down cleanly. Pair with the errgroup docs.

go.dev/blog/pipelines
errgroup docs

Check yourself (from memory)

Q1. errgroup's Wait() returns…

First non-nil error; its shared context is cancelled so sibling goroutines stop. Our bootstrap uses it for the HTTP+gRPC servers.

Q2. Graceful shutdown in our services begins with…

The signal cancels the root context, which cascades: cancel → Wait for goroutines → close resources.

Q3. A panic in a goroutine with no recover

Unrecovered goroutine panics take down the process — which is why long-lived workers defer a recover.
Narrate our Kafka engine's graceful shutdown, step by step.
recall, then click to reveal
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.
Want to build a bounded worker pool or a cancellable pipeline from scratch as an exercise? Ask me — great hands-on practice.

1. Go Concurrency Patterns: Pipelines and cancellation; errgroup.