Ran into a simple thing and spent longer on it than I'd like.

A pool of eight workers reads tasks from jobs and writes results to an unbuffered results. The consumer reads results in a loop and returns on ctx.Done() at the first error. The cancel arrived, the consumer left. And the workers that had already computed their result are still standing on results <- r. Nobody is left to read that channel, and they'll stand there until the process ends.

The cancel by itself does nothing here. ctx.Done() is just a channel that gets closed. If a goroutine isn't listening to it at the spot where it waits, it never hears about the cancel.

The fix is short. Wrap the send in a select with two branches, case results <- r and case <-ctx.Done(), and return in the second. The Stream example in the context docs is built the same way, I just hadn't noticed it before.

Java has a similar picture with SynchronousQueue.put, except there you can at least call interrupt on the thread, and put throws InterruptedException. Go has no such outside lever. The goroutine has to look at ctx itself.

I think the rule is simple. Every place a worker can wait also listens for the cancel.