7. Channels & Message-Passing Concurrency

Native channel types (or their closest analog), buffered vs. unbuffered semantics, multiplexing over several channels at once, and how to signal completion.

Go and Kotlin both have a first-class Channel type built for exactly this job. Java has no channel type at all -- BlockingQueue is the closest analog, but it's a queue you poll, not a first-class concurrency primitive with select-style multiplexing. Python's queue.Queue and asyncio.Queue play the same role. JavaScript has no in-process channel concept at all (there's rarely a second thread to pass messages to), but postMessage between workers is a real, structurally similar message-passing primitive.

See roadmap: Coordination Primitives

Language Verdict: Pros, Cons & Recommendation

Go

5/5
  • chan and select are first-class language syntax, not library calls -- the strongest channel support of the five
  • close() + range give complete, built-in graceful-completion semantics
  • No unbounded channel option -- always an explicit, deliberate capacity choice (arguably also a pro)
  • Sending on / closing an already-closed channel panics -- sharp edges to respect

Java

2/5
  • BlockingQueue's bounded-capacity variants (Array/Linked/Synchronous) cover most practical needs
  • Mature, battle-tested, part of java.util.concurrent since Java 5
  • No select-equivalent for multiplexing multiple queues without polling
  • No close()/completion signal -- poison-pill sentinels are boilerplate you write every time

Kotlin

5/5
  • First-class, coroutine-suspending Channel with select{} multiplexing, closing the exact gaps Java's BlockingQueue has
  • CONFLATED capacity mode is a genuinely unique, useful option none of the other four offer
  • One more concurrency vocabulary (Channel + select) layered on top of Java interop options
  • Coroutine-only -- not usable directly from plain-thread code without a bridge

Python

3/5
  • Unified maxsize-bounded API shared between thread-based Queue and coroutine-based asyncio.Queue
  • asyncio.wait(FIRST_COMPLETED) covers the multiplexing need, if more verbosely than select
  • maxsize=0 means UNBOUNDED, the inverse convention of Go's 0-capacity channel -- a real gotcha
  • No close()/completion signal on either queue type -- same poison-pill boilerplate as Java

JavaScript

2/5
  • MessageChannel/MessagePort gives a real, structured message-passing primitive between actual threads
  • Promise.race covers ad-hoc multiplexing when sources are already Promises
  • No channel concept at all for the common single-threaded case -- everything is hand-rolled
  • MessagePort.close() is abrupt, no graceful drain-then-close semantics like Go/Kotlin
Recommendation: If channels are central to your design, Go and Kotlin give you the most complete, first-class support (select, close, capacity semantics); in Java or Python, be ready to explicitly name the workaround (poison pills, poll-based multiplexing) rather than assuming an equivalent exists.

Concurrency Mechanics, Side by Side

Native Channel Types (or the Closest Analog)

Must-know
ch := make(chan Item, 100) // buffered ch <- item // blocks if full item := <-ch // blocks if empty for item := range ch { process(item) } // iterate until closed

chan is a built-in language type with its own operator syntax (<-), not a library class -- this is the strongest first-class channel support of any language in this manual, reflecting Go's stated philosophy: 'don't communicate by sharing memory; instead, share memory by communicating'.

Buffered vs. Unbuffered / Bounded vs. Unbounded

Must-know
make(chan Item) // unbuffered -- send blocks until a receiver is ready make(chan Item, 10) // buffered, capacity 10 -- send only blocks once full // No unbounded channel option exists -- you must pick a finite capacity (or 0).

An unbuffered channel (the zero-value/no-capacity-argument form) forces the sender and receiver to synchronize precisely at the handoff -- this is frequently used deliberately as a synchronization point, not just a data-passing mechanism. Go deliberately provides no unbounded-channel option; you always pick a concrete, finite capacity (including 0), forcing an explicit decision about backpressure that Java's LinkedBlockingQueue's default lets you skip (dangerously).

Multiplexing: Waiting on Several Channels at Once

Must-know
select { case a := <-channelA: fmt.Println("A:", a) case b := <-channelB: fmt.Println("B:", b) case <-time.After(time.Second): fmt.Println("timed out") default: fmt.Println("nothing ready right now") // makes the whole select non-blocking

select is a first-class Go statement (not a function call) built specifically for this -- and it's the origin of the pattern Kotlin's select{} is deliberately modeled after. The optional default case is what turns a blocking select into a non-blocking poll, exactly analogous to Go's earlier TryLock/non-blocking-send patterns.

Closing a Channel & Signaling Completion

Recommended
close(ch) // signals 'no more sends' -- a second close panics for item := range ch { process(item) } // ends automatically once drained item, ok := <-ch // ok is false if the channel is closed AND drained

close(ch) is the direct inspiration for Kotlin's Channel.close() and works identically: a range loop ends naturally, and the two-value receive form's second return value tells you definitively whether you got a real item or the channel is exhausted. Closing an already-closed channel panics, and sending on a closed channel panics too -- both sharp, well-known Go gotchas.

Further Reading