Answer

Use `select` with `ctx.Done()` on every channel send: ```go func producer(ctx context.Context, ch chan<- Result) { for { result, err := doWork(ctx) if err != nil { return } select { case ch <- result: // sent successfully case <-ctx.Done(): return // context cancelled, exit cleanly } } } ``` **On the race question:** When both cases are ready simultaneously, Go's `select` picks one **uniformly at random**. So yes, there's a ~50% chance the send fires even when the context is already cancelled. This is fine — one extra item in the channel is harmless. The important thing is the goroutine **will** exit on the next loop iteration. **Buffered channels:** With a buffered channel, the send can succeed without a reader. The goroutine might send one more item after cancellation, but it won't block — it'll hit `ctx.Done()` on the next iteration and exit. **The idiomatic pattern is:** 1. Always `select` between the channel op and `ctx.Done()` 2. The consumer should also `select` on `ctx.Done()` when reading 3. Close the channel from the producer side when done (signals consumer) 4. Never close from the consumer side For fan-out (multiple producers, one consumer), use `sync.WaitGroup` to close the channel after all producers exit.

a53ba7aa-b3c0-4919-9059-93040245fe91

Use select with ctx.Done() on every channel send:

func producer(ctx context.Context, ch chan<- Result) {
    for {
        result, err := doWork(ctx)
        if err != nil {
            return
        }
        select {
        case ch <- result:
            // sent successfully
        case <-ctx.Done():
            return // context cancelled, exit cleanly
        }
    }
}

On the race question: When both cases are ready simultaneously, Go's select picks one uniformly at random. So yes, there's a ~50% chance the send fires even when the context is already cancelled. This is fine — one extra item in the channel is harmless. The important thing is the goroutine will exit on the next loop iteration.

Buffered channels: With a buffered channel, the send can succeed without a reader. The goroutine might send one more item after cancellation, but it won't block — it'll hit ctx.Done() on the next iteration and exit.

The idiomatic pattern is:

  1. Always select between the channel op and ctx.Done()
  2. The consumer should also select on ctx.Done() when reading
  3. Close the channel from the producer side when done (signals consumer)
  4. Never close from the consumer side

For fan-out (multiple producers, one consumer), use sync.WaitGroup to close the channel after all producers exit.