channels and select
A channel is a pipe between concurrent tasks, like a conveyor belt between two workstations: one side puts items on, the other side takes them off, and the belt itself handles the safe handoff so the two workers never have to touch each other's hands. In code, a channel is a thread-safe queue with two ends — a sender and a receiver — used to pass values from one task to another. It is the concrete tool behind 'do not communicate by sharing memory; share memory by communicating': instead of two tasks fighting over a shared variable with a lock, one task sends the value down a channel and the other receives it.
Channels come in variants worth knowing. Bounded versus unbounded: a bounded channel has a fixed capacity, so when it is full a send blocks (or suspends) until the receiver makes room — this is built-in backpressure. An unbounded channel never blocks the sender but can grow without limit if the receiver falls behind (a memory-leak risk). The endpoint counts give the shorthand names: MPSC means many producers, single consumer (the classic work-funnelling channel); MPMC means many producers, many consumers (a shared work queue several workers drain). The transfer is the synchronisation point — handing a value over a channel safely publishes it to the receiver, so you do not need a separate lock. select is the companion operation: when a task needs to wait on several channels at once — 'whichever of these has something first, give it to me' — select blocks on multiple channel operations and proceeds with the first one that becomes ready (and can include a timeout or a cancellation channel as one of the arms).
Channels matter because they turn shared-state coordination into a dataflow: producers and consumers stay decoupled, the channel owns the handoff, and a bounded channel gives you flow control for free. Go's goroutines-plus-channels and Rust's std/tokio channels are built on this. The honest caveats: unbounded channels quietly defer the backpressure problem until you run out of memory; a channel where every sender is gone yields a 'closed' signal the receiver must handle; and select that does not handle a closed or cancelled arm can spin or hang. A channel is a coordination tool, not a guarantee of correctness on its own.
Go select waits on whichever is ready first: select { case v := <-ch1: use(v); case ch2 <- x: ; case <-time.After(d): timeout() }. A bounded channel make(chan T, 8) blocks the sender when 8 items are unconsumed — automatic backpressure.
select multiplexes several channel operations; a bounded channel provides backpressure when the receiver lags.
An unbounded channel does not remove backpressure — it just hides the cost until you run out of memory. And a channel is not a lock-free magic wand: receiving from a closed/empty channel and select with no ready arm are cases you must handle, or the task hangs.