An unbuffered send waits for a receiver¶
Level: 101 · anyone who has written ch <- v and wondered when it returns
One line: A channel made without a capacity holds nothing, so a send on it completes only when a receiver takes the value: the send is a handshake that keeps the sender waiting until the two goroutines meet — and a buffer with room in it lets the same send return at once.
main hands one order to a packer goroutine and waits for a receipt. Before the packer takes the order, it looks at where main is waiting — not by timing anything, but by reading the goroutine dump that the runtime prints when a program crashes, in which every parked goroutine is labelled with what it is waiting for. The program does this twice: with make(chan string), and with make(chan string, 1).
Verified output of an_unbuffered_send_waits_for_a_receiver_go.go — regenerated by tools/run_examples.py, never hand-typed.
orders with cap 0:
when the packer came to receive, main was parked in [chan send]
main got back "packed order-17"
orders with cap 1:
when the packer came to receive, main was parked in [chan receive]
main got back "packed order-17"
Reading the output¶
With cap 0, main was parked in [chan send]. It had reached orders <- "order-17" and stopped inside the send, and it stayed there until the packer received. The spec ↗ gives the rule in one sentence: when the capacity is zero, communication succeeds only when both a sender and a receiver are ready. Whichever side arrives first waits for the other; a receive on an unbuffered channel with no sender waits in the same way, in [chan receive].
With cap 1, main was parked in [chan receive]. The send copied the order into the buffer and returned before any goroutine had received it, and main went on to <-receipts, where it waited for the packer's reply instead. A buffered channel is a bounded queue follows that kind of send to the point where the buffer is full and it has to wait after all.
The answer does not depend on the scheduler. Nothing here decides which goroutine runs first. The packer polls the dump until main is parked on a channel, and the first channel operation main can be parked in is the send when there is no buffer, and the receipt when there is one. The labels are the runtime's own wait reasons, listed in runtime2.go ↗; All goroutines are asleep shows them again, in a crash.
The receipt is a second handshake. receipts has no buffer either, so main prints nothing until the packer has replied. That is how a goroutine, which has no handle to join, hands something back. It is also why main may read seen, a plain variable the packer wrote: the Go memory model ↗ guarantees that a send on a channel is synchronized before the corresponding receive completes, so what the packer wrote before its send is there when main's receive returns.
The memory model states this page's rule in the same terms: a receive from an unbuffered channel is synchronized before the corresponding send completes. So when an unbuffered send returns, whatever the receiver did before its receive has already happened. The memory model's example program depends on exactly that, and it notes that with make(chan int, 1) the same program would no longer be guaranteed to work.
an_unbuffered_send_waits_for_a_receiver_go.go
an_unbuffered_send_waits_for_a_receiver_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// An unbuffered channel is a handshake: a send does not complete until a
// receiver takes the value. This program hands one order to a packer goroutine
// and asks the runtime where main was waiting when the packer came to receive
// it -- first on an unbuffered channel, then on a channel with room for one.
//
// go build an_unbuffered_send_waits_for_a_receiver_go.go && ./an_unbuffered_send_waits_for_a_receiver_go
package main
import (
"fmt"
"runtime"
"strings"
"time"
)
func main() {
handOff(make(chan string))
handOff(make(chan string, 1))
}
// handOff sends one order to a packer and waits for the packer's receipt.
func handOff(orders chan string) {
receipts := make(chan string) // unbuffered: main waits here until the packer replies
var seen string // the packer writes it before its receive; main reads it after the receipt
go func() {
// Before taking the order, look at where main is waiting.
seen = waitingOnChannel("main.handOff")
order := <-orders
receipts <- "packed " + order
}()
orders <- "order-17"
receipt := <-receipts
fmt.Printf("orders with cap %d:\n", cap(orders))
fmt.Printf(" when the packer came to receive, main was parked in [%s]\n", seen)
fmt.Printf(" main got back %q\n", receipt)
}
// waitingOnChannel returns the runtime's own word for why the goroutine running
// fn is parked on a channel: "chan send" or "chan receive". It reads the same
// goroutine dump that a crash prints, and polls until that goroutine is parked
// on a channel; the sleep between polls only keeps the loop from spinning.
func waitingOnChannel(fn string) string {
buf := make([]byte, 1<<16)
for {
n := runtime.Stack(buf, true)
for _, g := range strings.Split(string(buf[:n]), "\n\n") {
header, frames, _ := strings.Cut(g, "\n")
if !strings.Contains("\n"+frames, "\n"+fn+"(") {
continue
}
_, state, _ := strings.Cut(header, "[") // "goroutine 1 [chan send]:"
state, _, _ = strings.Cut(state, "]")
state, _, _ = strings.Cut(state, ",") // a long wait reads "chan send, 2 minutes"
if strings.HasPrefix(state, "chan ") {
return state
}
}
time.Sleep(time.Millisecond)
}
}
What to do¶
- Use an unbuffered channel when the hand-over is the point — when the sender should not run on until someone has the value. Effective Go ↗ describes unbuffered channels as combining communication with synchronization.
- Do not add a buffer to stop a send from blocking until you have decided what should happen when the buffer is full. A buffer changes the order of events, not only the speed: with one, a send that returns no longer means that anyone has the value.
- Make sure a receiver will come. A send that no goroutine will ever receive waits forever. If every other goroutine is blocked too, the runtime stops the program (All goroutines are asleep); if not, the goroutine is simply stuck — A leaked goroutine never ends.
- To wait for several goroutines, count them instead of collecting a handshake from each: A WaitGroup counts goroutines.
In other languages¶
- The Rust library's Channels ↗ — Rust's default is the other way round: a send on
mpsc::channel()↗ never blocks, and this page's handshake ismpsc::sync_channel(0)↗, which its documentation calls a rendezvous channel. In both, ownership of the value moves with it. - The Concurrency library's Getting a result back ↗ — its Go section brings a goroutine's result back on a channel, where Rust and C take it from the join.
- The Concurrency library's chapter 05, Message passing, which will put channels and bounded queues to all of its languages, is planned.
Sources¶
- The Go specification: Channel types ↗ and Send statements ↗.
- The Go Memory Model, "Channel communication" ↗ — what happens before what, for buffered and unbuffered channels.
- Effective Go, "Channels" ↗.
runtime.Stack↗ — withallset to true, it formats the traces of all other goroutines after the caller's.- Katherine Cox-Buday, Concurrency in Go (O'Reilly, 2017), chapter 3, "Go's Concurrency Building Blocks" — the section on channels.
- Burak Serdar, Effective Concurrency in Go (Packt, 2023), chapter 3, "The Go Memory Model".