tezvyn:

Go Channels: Buffered vs. Unbuffered

AI-drafted, machine-checkedSource: go.devintermediate

Unbuffered channels are a synchronous rendezvous, blocking until both sender and receiver are ready. Buffered channels are an async mailbox, letting senders drop messages and go. The footgun is using a buffer to hide a deadlock instead of fixing it.

WHY IT EXISTS Goroutines need safe ways to communicate and synchronize. Sometimes you need a guarantee that a message was received before proceeding (synchronous). Other times, you want to avoid one goroutine's slowness from blocking another (asynchronous). Channels provide both mechanisms through their buffering strategy.

THE MENTAL MODEL An unbuffered channel is a direct, synchronous handoff. Think of a relay race: the runner with the baton cannot move on until the next runner grabs it. Both parties must be present for the exchange. A buffered channel is a mailbox or a conveyor belt. You can drop off a limited number of items and walk away. The sender only waits if the mailbox is full; the receiver only waits if it's empty.

HOW IT WORKS An unbuffered channel is created with make(chan T). A send operation on it will block until another goroutine is ready to receive. Likewise, a receive will block until a sender is ready. This forces a synchronization between the two goroutines. A buffered channel is created with a capacity, like make(chan T, 10). Sends will only block if the buffer is full, and receives will only block if the buffer is empty. The sender and receiver can operate at different times, as long as the buffer has space or items.

WHEN TO USE IT Use unbuffered channels for guaranteed synchronization. They are perfect for signaling when a task is complete, as the sender knows the receiver has acknowledged the signal. Use buffered channels to decouple producers and consumers. This is ideal for worker pools where a main goroutine queues up tasks for multiple workers, smoothing out bursts of work and improving throughput.

WHEN NOT TO USE IT Do not use a buffered channel when you need a strong guarantee of synchronization; the sender has no idea when the message is actually processed, only that it was queued. Critically, do not add a buffer simply to "fix" a deadlock. A deadlock with an unbuffered channel often points to a fundamental logic error in your concurrency design. Adding a buffer may just hide the problem, making it intermittent and harder to debug.

ONE CANONICAL EXAMPLE A worker pool is a classic use case for buffered channels. A main goroutine creates a buffered channel of jobs, jobs := make(chan Job, 100). It then loops, sending jobs into the channel. Several worker goroutines are started, all reading from this same jobs channel. The buffer allows the main goroutine to quickly queue up 100 jobs without waiting for a worker to be free, decoupling job creation from job execution.

Read the original → go.dev

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.