Buffered vs unbuffered channels in Go
An unbuffered channel is a handshake. A buffered channel is a small queue.
- 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 when | No receiver is ready | The buffer is full |
| Receive blocks when | No sender is ready | The buffer is empty |
| Guarantee | Sender knows the value was received | Sender only knows it was queued |
cap(ch) | 0 | n |
| Good for | Handoffs and signals where timing matters | Known number of results, semaphores, smoothing short bursts |
| Risk | Deadlock if nobody receives | Hides 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.
package mainimport "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.
ch := make(chan int)ch <- 1 // blocks forever: main is the only goroutine, and it is stuck herefmt.Println(<-ch)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.
package mainimport ( "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())}- 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.
- 1What does
ch := make(chan int, 1); ch <- 1; ch <- 2do inmainwith no other goroutines? - 2Change the semaphore size in
semaphore.goto 1. What is printed? - 3You start 5 goroutines that each send one result. What buffer size lets them all finish even if you read results later?