Context Cancellation
A goroutine you cannot stop is a leak. The last three concepts started goroutines and waited for them to finish on their own terms; real programs need the other direction too - a request is abandoned, a user closes the tab, the server is shutting down, and every goroutine working on that job should notice and stop.
context.Context is how Go says "stop".
One value, passed down through every call, carrying a single broadcast: this
work is no longer wanted.
The shape of it
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
context.Background() is the empty
root - no deadline, never cancelled, the context you start from in main or a
test. WithCancel derives a child
from it and hands you the switch. Contexts form a tree: cancelling a parent
cancels every descendant, and cancelling a child leaves the parent alone.
Three things a context gives you, and only three:
ctx.Done()- a channel that is closed when the work should stop. A closed channel is receivable for ever by every goroutine at once, which is exactly what a broadcast needs.ctx.Err()-nilwhile the context is live, thencontext.Canceledorcontext.DeadlineExceededonce it is not.ctx.Value(key)- request-scoped data, and the subject of the third challenge in this concept.
Listening for it
Cancellation is a channel, so it composes with everything you learned about
select:
select {
case v := <-work:
return v, nil
case <-ctx.Done():
return 0, ctx.Err()
}
Two rules from that snippet are worth stating on their own.
Return ctx.Err(), not an error of your own. errors.Is(err, context.Canceled) is how a caller several frames up tells "the user went away"
from "the database is broken", and a hand-written
errors.New("cancelled") throws that away.
A send needs the same treatment as a receive. out <- v blocks until
somebody receives; if the consumer has already given up, that goroutine is
parked for the lifetime of the process. Wrap it in the same select.
Checking without waiting
select with a default never blocks: if no case is ready, the default runs
immediately.
select {
case <-ctx.Done():
return ctx.Err()
default:
}
That is the check to put at the top of each iteration of a long loop, so a cancelled job stops at the next step instead of at the end.
Conventions
ctxis the first parameter, namedctx, and typedcontext.Context- not stored in a struct.- The function that creates a context calls
cancel, always, usually withdefer. Skipping it leaks the timer and the child bookkeeping until the parent dies.go vetcalls this out as "the cancel function is not used". - Never pass a
nilcontext.context.TODO()is the placeholder for "there will be one here later".
Task
Implement Wait, Cancelled, CountTicks and Produce.
Hints
Waitis oneselectwith two cases, and the cancelled case returnsctx.Err().Cancelledis the sameselectwith adefaultand no work in it.CountTicksneeds the two-value receive: a closedticksis always ready, sov := <-ticksalone would spin for ever counting zeros.Producestarts a goroutine,defer close(out)inside it, and sends each value through a select againstctx.Done(). Return the channel straight away - the sends happen after you have returned.