Go’s slogan is the whole idea:
Do not communicate by sharing memory; share memory by communicating.
Instead of two goroutines both touching a variable behind a lock, one sends the value to the other. Ownership moves with the message, so at any moment exactly one of them has it.
A channel is a typed pipe
results := make(chan int)
go func() { results <- expensiveWork() // send}()
value := <-results // receive; blocks until something arrivesNo mutex, no shared variable, no visibility question. The channel handles all three.
Buffered or not is a real decision
make(chan T)
A send does not complete until a receiver takes the value, so the two goroutines meet. That makes the channel a synchronisation point as well as a pipe — after the handoff, both know the other reached this line.
Unbuffered (make(chan int)) — a send blocks until a receiver takes it. The
two goroutines rendezvous, so the channel is a synchronisation point as well
as a pipe: after the handoff, each knows the other reached that line.
Buffered (make(chan int, 3)) — the sender can run up to three ahead. That
decouples their speeds, and the capacity is your
backpressure budget.
select is what makes it a model rather than a queue
Wait on several channels at once, and act on whichever is ready first:
select {case job := <-work: process(job)case <-done: return // shut downcase <-time.After(5 * time.Second): log.Println("idle for five seconds")}Timeouts, cancellation and fan-in all fall out of this one construct. Without it, channels are just queues; with it, they are a way to structure a program.
The mistakes
Goroutine leaks. A goroutine blocked forever on a channel nobody will use
never returns, and its stack and captured variables are never freed. This is the
Go equivalent of a memory leak and it is the most common bug in channel-heavy
code. Always give a blocked goroutine an exit — a done channel, or a
context.Context.
Deadlock by channel. Two goroutines each waiting to receive from the other is the same cycle as any deadlock. Go’s runtime detects the case where all goroutines are blocked and panics with a clear message; partial deadlocks it cannot see.
CSP and actors
Both avoid shared memory by passing messages. The difference is what has a name:
| CSP | Actors | |
|---|---|---|
| Named thing | the channel | the actor |
| Processes | anonymous | have addresses |
| Default send | synchronous (unbuffered) | asynchronous |
| Buffering | explicit, at the channel | a mailbox, always present |
| Across a network | awkward — rendezvous needs both ends | natural |
CSP suits pipelines inside one process. Actors suit distributed systems, where an address survives a machine boundary and a rendezvous does not.