Lesson 17 · Concurrency & context

context.Context

The first argument in every function you've seen — finally explained.

Your win: use context for cancellation, deadlines, and request-scoped values; state the conventions; and explain the deliberate Background() choice in our consumer retries.

One value, three jobs

A context.Context travels as the first argument through a request's call tree and carries three things:1

Cancellation

A Done() channel that closes when work should stop. ctx.Err() says why.

Deadlines / timeouts

WithTimeout/WithDeadline auto-cancel after a duration/time.

Request-scoped values

WithValue carries identity, trace IDs — data that belongs to this request.

ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()                 // always — releases resources (Lesson 13)

select {
case <-ctx.Done(): return ctx.Err()  // cancellation is COOPERATIVE — you must check
case r := <-work: return r
}
Cancellation is cooperative Cancelling a context does not kill goroutines — it closes Done(). Your code must select on it (or pass ctx to functions that do) to actually stop. A goroutine that ignores ctx runs to completion regardless.

Conventions (interviewers check these)

Your codebase, from key to Kafka

Context values use private key types so packages can't collide, with typed getters/setters:

internal/golibs/interceptors/auth.go:346-477
type UserIDKey int   // unexported type → no cross-package key collision
ctx = interceptors.ContextWithJWTClaims(ctx, claims)
uid := interceptors.UserIDFromContext(ctx)
Anchor — context survives a hop through Kafka When our services publish, the claims/trace ride in message headers; on consume, TraceCarrierAndClaimInfoFromMessageHeaders rebuilds them and re-injects into a fresh ctx (consumer.go:114-131). So a handler runs with the same identity the producer had — context propagation across an async boundary.
A deliberate subtlety worth explaining in an interview Our consumer builds each attempt's timeout from context.WithTimeout(context.Background(), retryTimeout) (consumer.go:112) — not from the incoming ctx. Why? So an upstream cancellation can't kill an in-flight retry mid-work. The parent consumer context still governs the retry loop; each attempt gets its own clean deadline. Intentional, and a great "why did they do that?" talking point.
Read this next

The Go Blog — "Go Concurrency Patterns: Context"

The canonical explanation of cancellation propagation and request-scoped values. Then skim the context package docs for the API.

go.dev/blog/context
pkg.go.dev/context

Check yourself (from memory)

Q1. context.Context's three jobs are…

A cancellation signal, an optional deadline, and request-scoped values — carried as the first argument.

Q2. Cancellation via context is…

Cancel closes Done(); nothing stops unless code selects on it or passes ctx down. Ignore ctx → runs to completion.

Q3. Context values should carry…

Identity, trace IDs — data tied to the request. Not a substitute for explicit parameters. Use private key types to avoid collisions.
Why build the consumer's per-attempt timeout from context.Background() instead of the incoming ctx?
recall, then click to reveal
So an upstream cancellation doesn't kill an in-flight retry mid-work. Each attempt gets a fresh WithTimeout(context.Background(), retryTimeout), while the parent consumer context still governs the overall retry loop (via try.DoWithCtx selecting on Done()). A deliberate choice to make retries robust — and a strong "why did they do that?" interview answer.
Want to see context.WithValue misused (the anti-pattern) vs. our clean private-key approach side by side? Ask me.

1. The Go Blog — Go Concurrency Patterns: Context; context package docs.