3 ms·
People are missing a key insight about channels, which is that close is a write. Typically, you want one piece of code reading, and another piece of code writi
by jrockway 2y ago
People are missing a key insight about channels, which is that close is a write. Typically, you want one piece of code reading, and another piece of code writing, and that's where their code goes wrong. A panic is appropriate in this case, as it's always a bug.
The underlying feature that people are looking for is a TCP-like "either side can kill the other side" feature. This is what contexts are.
func read(ctx context.Context, c <-chan any) {
for {
select {
case <-ctx.Done():
return
case x, ok := <-c:
if ok {
fmt.Println(x)
} else {
return
}
}
}
}
func write(ctx context.Context, c chan<- any) {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
close(c)
return
case c <- i:
}
}
}
In this example, the type system prevents the read function from writing to the channel (avoiding the panic in the writer if the reader were to close the channel), and the context causes both sides to exit cleanly. You can plumb in the cancel function returned from context.WithCancel/context.WithCancelCause to allow the reader to kill the writer. (Incidentally, the reason you have to do this is because context.Done() is type <-chan, not type chan, so you can't just close(ctx.Done()).)
The writer can still kill the reader by closing the channel, because closes are writes.
- akira2501 2y agoMore importantly close is a _broadcasting_ write. Which is an incredibly useful feature when you want to build nested structures out of your channel paths.
- jrockway 2y agoYup! This is exactly how context.WithCancel is implemented; <-ctx.Done() is the signal that everything that cares about cancellation listens on. The returned CancelFunc just does `close(thatChannel)`. I prefer explicitly using contexts for cancellation, but in a pinch: c := make(chan struct{}) foo(c) bar(c) close(c) is a useful pattern. (I do this a lot in tests that involve concurrency; the test calls t.Cleanup(func() { close(c) }) and then if the test fails with t.Fatal or something, all background work is killed.)
- mappu 2y agoExactly right. These "done channels" were extremely widely used as a doneCh parameter before contexts were introduced in 1.7.