So, as explained in this question, select statement chooses a channel operation at random. And I often see a pattern like this:
func foo(ctx context.Context, someChannel <-chan int) {
for {
select {
case someValue := <-someChannel:
//do some stuff until the context is done
expensiveComputation(someValue)
case <-ctx.Done():
//context is done - either canceled or time is up for timeout
return
}
}
}
but if we can not guarantee, which case
in select
will fire, it is possible, that case <-ctx.Done()
will be selected not instantly, but after a couple of iterations of expensiveComputation(someValue)
, witch we actually don't need to do anymore, because context is canceled. It is also possible that it will never get selected, but that's too low of a probability...
Go spec also says that "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 what's also possible is:
someChannel
now in the select statement there will be two operations, that can proceed, available, and one of them will be chosen at random, and again, some computation will potentially be done after context has been canceled.
So how to properly deal with it? Or, if I'm missing something here, what is it?
select statement chooses a channel operation at random
Randomly selects one channel that is ready for reading.
it is possible, that case <-ctx.Done() will be selected not instantly, but after a couple of iterations of expensiveComputation(someValue), witch we actually don't need to do anymore, because context is canceled
So you're describing a situation where the writer to someChannel
continues to write or the channel's buffer is not empty, even though the context is cancelled. If someChannel
is not ready for reading it will not be selected (unless it's closed, as you say, more on that below). I think you get that, I just wanted to make it explicit that a channel not ready for reading is never selected.
expensiveComputation(someValue)
In real life, if you want cancellation to be effected quickly, you'd pass the context to your expensiveComputation
so that it could then respect its cancellation. expensiveComputation
could then return an indication of whether the context was cancelled during expensiveComputation
so that you can drop out of the select
immediately. That might look something like:
if ok := expensiveComputation(ctx, someValue); !ok {
// context has been cancelled during `expensiveComputation`
return
}
A receive operation on a closed channel can always proceed immediately
To quote the tour (which you should take if you haven't yet!)
Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression: after
v, ok := <-ch
ok
isfalse
if there are no more values to receive and the channel is closed.
Receiving from a nil channel blocks forever
So if you want to close channels that are read in a select
, you need to combine these two ideas: use the second value from the receive to know that the channel is closed, then set the channel to nil
so you don't keep reading zero values from it.
func foo(ctx context.Context, someChannel <-chan int) {
for {
select {
case someValue, ok := <-someChannel:
if ! ok {
someChannel = nil
// this case will never again be selected
} else {
expensiveComputation(someValue)
}
case <-ctx.Done():
//context is done - either canceled or time is up for timeout
return
}
}
}
In your case, someChannel
is the only source of work for foo
, but in some cases there may be multiple channels involved. In your case it might make more sense to return from foo
when the channel is closed.
case someValue, ok := <-someChannel:
if ! ok {
return // end foo, it will receive no more work
} else {
expensiveComputation(someValue)
}