Kotlin Channels Split Send and Receive Into Separate Interfaces — Here’s the Iterator That Powers the Loop
Separating send and receive into distinct interfaces makes it harder to accidentally consume from a producer or write from a consumer — a compile-time guard that matters once channels are passed across module boundaries. Understanding the iterator’s fast-poll-then-suspend path also explains why a for-in loop over a channel never burns CPU spinning on an empty queue.
A Kotlin Channel is really two interfaces: SendChannel for writing and ReceiveChannel for reading. The factory function picks one of four internal implementations — rendezvous, conflated, unlimited, or array-backed — all inheriting from AbstractChannel, which implements both interfaces and provides the iterator that makes `for (element in receiver)` work. That iterator, `Itr`, first tries a non-blocking poll via `pollInternal`; if the queue is empty, it falls back to a suspending path that enqueues a receive node and waits.
The internal queue is modeled on Java’s BlockingQueue but designed for coroutine suspension instead of thread blocking. Producers wrap elements into nodes and append them to the tail; consumers pull from the head. When the buffer is full or empty, the affected coroutine suspends rather than tying up a thread, and the channel’s close mechanism propagates a `Closed` sentinel through the queue to terminate iteration cleanly.
A working example shows a producer coroutine sending five integers through a sender reference and then closing the channel, while a consumer coroutine iterates with a plain for-loop that the compiler desugars into `hasNext()`/`next()` calls on the channel’s iterator.
Exposing SendChannel and ReceiveChannel as separate types is a capability-restriction pattern that Kotlin’s standard library bakes in at the type level — the same Channel instance satisfies both, but you choose which reference to pass to each coroutine.
The iterator’s two-phase `hasNext` (fast non-blocking poll, then slow suspend) is a practical optimisation that avoids suspension overhead when elements are already buffered, which matters for high-throughput pipelines.
ConflatedChannel’s capacity of -1 is a sentinel value, not a literal size; it signals merge-on-write semantics rather than a fixed slot count, which is easy to misread when scanning the Channel factory source.