Channels
Typed pipes between goroutines: unbuffered handoffs, buffered queues, closing, select, timeouts and the rules for nil and closed channels.
Section 15 of 31 6 min read
A channel is a typed conduit between goroutines. Sending and receiving synchronise the two sides, so a channel carries both data and coordination. The Go proverb: "Do not communicate by sharing memory; instead, share memory by communicating."
Creating and using channels
BlockingQueue<Message> queue = new ArrayBlockingQueue<>(10);
executor.submit(() -> {
queue.put(new Message("orders.created")); // blocks if full
return null;
});
Message msg = queue.take(); // blocks if emptych := make(chan Message, 10) // buffered, capacity 10
go func() {
ch <- Message{Topic: "orders.created"} // send: blocks if full
}()
msg := <-ch // receive: blocks if empty
fmt.Println(msg.Topic)Unbuffered vs buffered
// UNBUFFERED: capacity 0
done := make(chan struct{})
// A send blocks until a receiver takes the value, and vice versa.
// Both goroutines meet at the handoff: a synchronisation point.
// BUFFERED: capacity 100
jobs := make(chan Job, 100)
jobs <- Job{ID: 1} // returns immediately while there is room
// The 101st send blocks until a receiver makes space.
// That blocking is backpressure: fast producers slow down.
len(jobs) // items currently buffered
cap(jobs) // capacity: 100Unbuffered: the two sides meet
Buffered, cap 3: sends block only when full
Which one?
Unbuffered when the sender needs to know the value was received before it continues: signals, handoffs, request/response between goroutines.
Buffered to decouple a producer and consumer running at different speeds, sized for the bursts you expect. A buffer does not make a slow consumer faster; it only delays backpressure. If you are tempted to make a buffer huge "just in case", you probably have an unbounded queue problem.
Closing a channel: no more values
results := make(chan Result)
go func() {
defer close(results) // the SENDER closes, when it is done
for _, job := range jobs {
results <- run(job)
}
}()
// range receives until the channel is closed and drained
for r := range results {
fmt.Println(r)
}
// The comma-ok form tells you whether the channel is closed
r, ok := <-results
if !ok {
// closed and empty: r is the zero value
}| Operation | On a nil channel | On a closed channel |
|---|---|---|
OperationSend ch <- v | On a nil channelBlocks forever | On a closed channelPanics |
OperationReceive <-ch | On a nil channelBlocks forever | On a closed channelReturns remaining values, then the zero value with ok == false |
Operationclose(ch) | On a nil channelPanics | On a closed channelPanics (closing twice) |
Operationrange ch | On a nil channelBlocks forever | On a closed channelEnds after draining buffered values |
select: wait on several channels at once
for {
select {
case msg := <-urgent:
handleUrgent(msg)
case msg := <-normal:
handle(msg)
case <-ctx.Done():
return ctx.Err() // cancelled: stop the loop
}
}
// If no case is ready, select blocks until one is.
// If several are ready, one is chosen at random (no priority!).// Timeout: time.After returns a channel that fires once
select {
case res := <-results:
use(res)
case <-time.After(2 * time.Second):
return errors.New("timed out waiting for result")
}
// Non-blocking send: default runs when no case is ready
select {
case events <- ev:
// queued
default:
droppedEvents.Add(1) // buffer full: drop instead of blocking
}Directional channel types
// chan T bidirectional
// chan<- T send-only
// <-chan T receive-only
// A producer returns a receive-only channel: callers can
// read from it but cannot send to it or close it.
func generate(ctx context.Context, n int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := range n {
select {
case out <- i:
case <-ctx.Done():
return // consumer gave up: exit, don't leak
}
}
}()
return out
}
func sum(in <-chan int) (total int) {
for v := range in {
total += v
}
return total
}