Skip to content
JavaGo
Part IV · Concurrency

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

Java — BlockingQueue
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 empty
Go — channel
ch := 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 = rendezvous, buffered = bounded queue
// 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: 100

Unbuffered: the two sides meet

Buffered, cap 3: sends block only when full

Fig. 15.1Time runs left to right. On an unbuffered channel a send blocks until a receiver takes the value, so the two goroutines meet at the handoff. A buffered channel accepts sends immediately until it is full; only then does the producer wait, which is backpressure.

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

close() signals completion; range stops at close
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
}
OperationOn a nil channelOn a closed channel
OperationSend ch <- vOn a nil channelBlocks foreverOn a closed channelPanics
OperationReceive <-chOn a nil channelBlocks foreverOn a closed channelReturns remaining values, then the zero value with ok == false
Operationclose(ch)On a nil channelPanicsOn a closed channelPanics (closing twice)
Operationrange chOn a nil channelBlocks foreverOn a closed channelEnds after draining buffered values

select: wait on several channels at once

select is a switch whose cases are channel operations
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!).
timeouts and non-blocking operations
// 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

send-only and receive-only narrow the contract
// 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
}
↑ ↓ to navigateEnter to openEsc to close