Dev Skills

Buffered vs unbuffered channels in Go

An unbuffered channel is a handshake. A buffered channel is a small queue.

Topic 91 of 98 · 5 min to read · 20 min to build
After this topic you can
  • Explain when a send blocks on a buffered and an unbuffered channel.
  • Fix a deadlock caused by sending on an unbuffered channel with no receiver.
  • Use a buffered channel as a semaphore to limit concurrency.

make(chan int) is unbuffered: a send waits until someone receives. make(chan int, 3) is buffered: up to 3 sends go through without a receiver. Here is how they differ.

Unbuffered `make(chan T)`Buffered `make(chan T, n)`
Send blocks whenNo receiver is readyThe buffer is full
Receive blocks whenNo sender is readyThe buffer is empty
GuaranteeSender knows the value was receivedSender only knows it was queued
cap(ch)0n
Good forHandoffs and signals where timing mattersKnown number of results, semaphores, smoothing short bursts
RiskDeadlock if nobody receivesHides a slow consumer until the buffer fills

What is the difference between buffered and unbuffered channels?

An unbuffered channel makes sender and receiver meet: the send finishes only when the value is taken. A buffered channel has room for n values, so sends finish immediately until it is full. Unbuffered gives you synchronisation; buffered gives you slack.

The full channel rules (nil, closed, buffered) are in Channels and select.

both.go
package main
import "fmt"
func main() {
// Buffered: two sends succeed with nobody receiving yet.
b := make(chan string, 2)
b <- "first"
b <- "second"
fmt.Println("buffered len/cap:", len(b), cap(b))
fmt.Println(<-b, <-b)
// Unbuffered: the send needs a receiver, so it happens in a goroutine.
u := make(chan string)
go func() { u <- "handed over" }()
fmt.Println("unbuffered cap:", cap(u), "got:", <-u)
}

Why does sending on an unbuffered channel deadlock?

Because the send waits for a receiver, and if the only goroutine is the one sending, nobody ever receives. Go detects this and stops with "fatal error: all goroutines are asleep - deadlock!". Receive in another goroutine, or add a buffer if that fits the design.

deadlock.go
ch := make(chan int)
ch <- 1 // blocks forever: main is the only goroutine, and it is stuck here
fmt.Println(<-ch)
Do not "fix" every deadlock with a buffer. Usually the bug is that the receiver is missing.

What buffer size should I use?

Use 0 (unbuffered) unless you have a specific reason. Good reasons: you know exactly how many values will be sent (size it to that), or you are building a semaphore. A random size like 100 "for speed" usually just hides a slow consumer.

How do you limit concurrency with a buffered channel?

Make a buffered channel with room for n tokens. Each goroutine puts a token in before working and takes it out after. At most n can work at once, because the n+1th send blocks.

semaphore.go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
sem := make(chan struct{}, 3) // at most 3 at a time
var running, peak atomic.Int32
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{} // take a slot (blocks if 3 are busy)
defer func() { <-sem }() // give it back
n := running.Add(1)
for {
p := peak.Load()
if n <= p || peak.CompareAndSwap(p, n) {
break
}
}
time.Sleep(5 * time.Millisecond)
running.Add(-1)
}()
}
wg.Wait()
fmt.Println("most running at once:", peak.Load())
}
The short version
  • Unbuffered: a send waits for a receiver. It is a handshake.
  • Buffered: sends go through until the buffer is full. It is a queue.
  • Default to unbuffered; size a buffer only for a known reason.
  • "all goroutines are asleep - deadlock!" usually means a missing receiver.
  • A buffered channel of size n is a simple semaphore.
Remember: Unbuffered = both sides meet. Buffered = sender can run ahead by n values.
Then try these in your own editor
  1. 1What does ch := make(chan int, 1); ch <- 1; ch <- 2 do in main with no other goroutines?
  2. 2Change the semaphore size in semaphore.go to 1. What is printed?
  3. 3You start 5 goroutines that each send one result. What buffer size lets them all finish even if you read results later?