DevRef / Go / Channels
Go Channels: Buffered, Unbuffered, and Range
Channel semantics are simple to state and subtle in practice. The difference between buffered and unbuffered channels is not just a capacity parameter — it changes when a goroutine blocks, which changes the synchronisation guarantees your program provides.
Unbuffered channels: rendezvous synchronisation
An unbuffered channel (make(chan T)) requires both a sender and a receiver to be present before either can proceed. The send blocks until a goroutine calls receive; the receive blocks until a goroutine calls send. This is rendezvous synchronisation — both parties arrive at the channel simultaneously.
ch := make(chan int)
go func() {
ch <- 42 // blocks until someone receives
}()
val := <-ch // unblocks the sender
fmt.Println(val)
The guarantee: the send happens before the receive completes. This is not just a timing observation — it is a memory model guarantee. All writes visible to the goroutine before the send are visible to the goroutine after the receive. Unbuffered channels are a synchronisation primitive as much as a communication one.
Buffered channels: decoupled producer and consumer
A buffered channel (make(chan T, n)) has capacity for n values before any receiver is required. A sender blocks only when the buffer is full. A receiver blocks only when it is empty.
ch := make(chan int, 10)
ch <- 42 // does not block — space in buffer
ch <- 99
val := <-ch // receives 42 (FIFO)
fmt.Println(val)
Buffered channels decouple producer and consumer timing. They are also useful as semaphores: a channel of capacity N limits concurrent access to N slots simultaneously.
sem := make(chan struct{}, 10)
for _, item := range items {
sem <- struct{}{} // acquire: blocks when 10 goroutines are running
go func(item Item) {
defer func() { <-sem }() // release on exit
process(item)
}(item)
}
Closing and ranging over channels
Closing a channel signals to receivers that no more values will be sent. A receive on a closed channel returns the zero value immediately, with an optional second boolean indicating whether the channel was closed.
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch { // range exits when ch is closed and drained
fmt.Println(v)
}
Only the sender should close a channel. Closing a channel from the receiver side, or closing an already-closed channel, panics. If multiple goroutines are sending, coordinate closure with a sync.WaitGroup — a single goroutine waits for all senders to finish, then closes.
Directional channel types
Functions that only send or only receive can express this in their parameter types. chan<-T is send-only; <-chan T is receive-only. Bidirectional channels convert to directional types implicitly; the reverse is a compile error. This is a lightweight API contract enforced by the compiler.
func produce(out chan<- int) { // can only send
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consume(in <-chan int) { // can only receive
for v := range in {
fmt.Println(v)
}
}