select and context.Context Done channel

Viewed 1477

I don't understand how the Done() channel in context.Context can work as intended. The module documentation (and source code using it) relies on this pattern:

select {
case <-ctx.Done():
    return ctx.Err()

case results <- result:
}

The channel return by Done() is closed if the Context is canceled or timed out, and the Err() variable holds the reason.

I have two questions regarding this approach:

  1. What's the behavior of select when a channel is closed? When and why is the case entered? Does the fact that there's no assignment have relevance?

  2. According to the language reference:

    If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.

    If the choice is random, how does that pattern guarantee that I won't be sending results down a pipeline when the Context is canceled? I would understand if the cases were evaluated in declaration order (and closed channel cases were selected).

If I'm completely off the track here, please explain this to me from a better angle.

1 Answers

This case:

case <-ctx.Done():

Has the communication op:

<-ctx.Done()

It's a receive from a channel. Spec: Receive operator:

A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.

So when the channel returned by ctx.Done() is closed, a receive from it can proceed immediately. Thus control flow can enter into this case.

If the other case (results <- result) can also proceed when the context is cancelled, one is chosen (pseudo-)randomly, there is no guarantee which one it will be.

If you don't want to send a value on results if the context is already cancelled, check the ctx.Done() channel before the select with another non-blocking select:

select {
case <-ctx.Done():
    return ctx.Err()
default:
}

select {
case <-ctx.Done():
    return ctx.Err()

case results <- result:
}

Note that you must add a default branch to the first select, else it would block until the context is cancelled. If there is a default branch and the context is not yet cancelled, default branch is chosen and so the control flow can go to the second select.

See related questions:

Force priority of go select statement

How does select work when multiple channels are involved?

Related