Skip to content

Step 1 of 5 · Reading · ~4 min

Learn

Concurrency at Scale

context.Context

You will stop a tree of goroutines with one call, bound work with a deadline, and tell from ctx.Err() which of the two happened.

In Go Intermediate you stopped goroutines by closing a done channel and bounded waits with time.After. A context.Context packages both into one value that also carries a deadline and a few request-scoped values, forms a tree, and is the first argument every standard-library API expects.

The interface and the tree

A Context has four methods: Done() (a channel closed when the context ends), Err() (nil until then, afterwards context.Canceled or context.DeadlineExceeded), Deadline() and Value(key). You derive contexts from a root: context.Background() in main and tests, context.TODO() for "not wired up yet". The root never ends, so its Done() is a nil channel that never closes.

go
<nil> <nil>
context canceled context canceled

Cancelling a parent cancels every descendant, and each descendant's own Err() reports it. The reverse never happens: cancelChild() leaves parent.Err() nil. WithTimeout(p, d) is WithDeadline(p, time.Now().Add(d)), and a child never outlives its parent's deadline. When the timer fires, Err() becomes context.DeadlineExceeded (text context deadline exceeded); test for it with errors.Is, as with any sentinel error.

Always call cancel

WithCancel, WithTimeout and WithDeadline each return a cancel function. Call it, normally with defer cancel() on the very next line. Without it the context stays registered with its parent, and its timer keeps running, until the parent ends or the deadline fires: one leak per request. Calling it twice is harmless; go vet, the checker bundled with Go, flags a lost one.

Select on Done around every block

Checking ctx.Err() at the top of a loop is not enough: a goroutine blocked on a channel operation never reaches the top again. Put the blocking operation inside a select next to <-ctx.Done(). Here the blocking operation is a receive. in <-chan int is a channel you may only receive from (the arrow points out of the channel); chan<- int is send-only. This consumer adds numbers until the producer closes in or the context ends:

go

Feed it two numbers from a producer that then stalls without closing, with a 20 ms deadline:

go
sum: 10 err: context deadline exceeded

With a bare n := <-in, total would wait forever. runtime.NumGoroutine() returns how many goroutines are alive right now; main alone is 1, so 2 after everything should have finished means one is stuck. That is the leak check.

When two select cases are ready at once, Go picks one at random, so a send racing a cancel may deliver one more value. The goroutine still exits on its next pass.

Values

WithValue attaches one key and one value to a new context. Value walks from the context up through its parents until a key matches, so a value stored on the parent is visible from every child, and a key nobody stored returns nil.

go
ana true 0 false

Value returns interface{}, so you convert it with a type assertion. The two-result form gives the zero value and false for a missing key; without , ok, child.Value(retryKey).(int) panics: interface conversion: interface {} is nil, not int. A bare string key such as "user" can collide with another package's "user", so the key is a type of your own. Values are for request-scoped data (request id, authenticated user), never for optional arguments.

Common mistakes

  • Checking ctx.Err() before a send instead of selecting on it. The goroutine is already blocked when the cancel arrives, so it never exits: the leak check prints leaked: true.
  • Returning on Done without closing the output channel. The goroutine exits but receivers wait forever: closed: false.
  • Asserting .(string) without , ok. A context with no id crashes the program instead of reporting no id.
  • Using a bare string as the key. The value is stored under one key and looked up under another, so a context that does carry an id reports request: none.

Your exercise

Write gen, a generator that stops the moment its context ends even while blocked mid-send, and requestID, which reads the id back out of a context. The starter holds the scenario runner, which cancels, times out and leak-checks your goroutine.

Up nextsync Primitives in Depth: Atomics, Cond, PoolConcurrency at Scale

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…