When a sender finds a receiver already parked on a channel, where is the value copied?
answer
- the send checks the waiter queue first
- the waiting record already names a destination
- the sender does the copying
- a queued receiver implies an empty buffer
- one copy, one lock, then ready the waiter
basics
~20 sThe sending goroutine copies the value straight into the parked receiver's destination variable, using the address in that receiver's wait record, then marks the receiver runnable. The channel's buffer is not used, even on a buffered channel.
solid answer
~50 sThe sender copies the value **directly into the parked receiver's destination**, then readies it — a hand-off, not a store-and-forward. Under the channel's lock, the send path first checks the receive queue; if a waiting receiver is there, its wait record already holds the address the value should land in, so the sender does the copy through that pointer (with a write barrier, since it is writing into another goroutine's stack) and calls `goready`. Nothing touches the ring buffer, and that is true for a buffered channel too, because a receiver can only be parked on a channel whose buffer is empty. The result is one copy instead of two and no second lock acquisition, which is why an unbuffered channel is not "slower for having no buffer" — a rendezvous is often the *cheapest* path through a channel.
code
go · 8 linesch := make(chan int)
go func() {
ch <- 42 // no receiver queued yet? this goroutine parks on ch's send queue
}()
v := <-ch // a sender already parked? its value is copied straight into v
_ = vgo deeper
Know that a channel transfers a copy of the value and that an unbuffered channel is a rendezvous between two goroutines. The internal copy path is not expected of you yet.
Explain the ordering of the send's fast paths — queued receiver, then buffer slot, then park — and that the sender itself performs the copy into the waiting receiver's destination before readying it.
Use this to correct the folklore in code review: an unbuffered channel is not slow for lacking a buffer, and capacity should be chosen for backpressure semantics rather than as an imagined copy optimisation.
Keep the team's mental model on semantics rather than runtime trivia. Channel capacity is a queueing and backpressure decision with real failure modes; the copy path is an implementation detail that may change and should never drive an API.
## The three paths through a send Under the channel's lock, a send tries these in order: 1. **A receiver is already parked.** Pop its wait record, copy the value through the destination pointer that record carries, ready the receiver, release the lock. The value never enters the channel. 2. **No receiver, but a free buffer slot.** Copy the value into the ring buffer's tail slot and return without parking. 3. **Neither.** Enqueue a wait record for this goroutine on the send queue — with a pointer to the value still living in the sender's frame — and park. A receive mirrors it: take a parked sender's value if one is queued, else take from the buffer, else park. The first path is the **direct hand-off**, and it is the one worth understanding, because it explains several things people otherwise memorise as folklore. ## Why the buffer is skipped even when there is one It is tempting to think a buffered channel always routes values through its buffer. It does not, and it cannot need to: a receiver only parks when the buffer is *empty*, so any channel that has a queued receiver has nothing buffered. Going through the buffer would mean writing a value into a slot and immediately reading it back out — two copies where one suffices. The mirror case is slightly more interesting. If a buffered channel is **full** and senders are parked on it, a receive takes the value at the head of the buffer (preserving FIFO order — the parked sender's value is newer than everything buffered), then moves the parked sender's value into the slot that just freed up at the tail and readies that sender. So the parked sender is served in the same operation, and ordering is still exactly the order values were sent. ## Writing into another goroutine's stack The destination in the hand-off is usually a local variable in the *receiving* goroutine's frame — one goroutine writing into another goroutine's stack, which is unusual in Go. The runtime does that copy with an explicit write barrier so the garbage collector cannot miss a pointer that has just been published into a stack it may already have scanned. This is invisible to you, but it is the reason the copy is done by a dedicated runtime routine rather than a plain assignment. The hand-off also carries the memory-model guarantee you rely on: everything the sender did before the send is visible to the receiver after the receive completes. ## What it buys - **One copy instead of two.** For a large struct sent over a channel this is measurable; for a pointer or a small header it is noise. - **One lock acquisition instead of two.** The transfer completes inside the sender's critical section; the receiver wakes with the value already in place and has nothing left to do. - **Locality.** The readied goroutine is placed in the waker's fast next-to-run slot, so it typically runs on the same thread very soon after — while the freshly written value is still in that core's cache. That combination is why the common belief "unbuffered channels are slow because there's no buffer to absorb bursts" is wrong about the mechanism. An unbuffered channel is a rendezvous, and a rendezvous *always* takes the direct path: the only cost is a park/unpark pair when the two sides do not arrive together. Adding a buffer does not make an individual transfer cheaper — it decouples the two goroutines' timing so the second side does not have to park at all when it is running late, which is a different benefit entirely. ## What this does not license This is an implementation detail of the runtime, not a semantic contract, and it should not change how you write code: - It does not mean the receiver sees the sender's variable by reference. The value is **copied**; if you send a struct containing a slice or a pointer, the header is copied and both goroutines then share the underlying array or pointee, exactly as with any other assignment in Go. - It does not make the send "synchronous" beyond what the channel type already says: on a buffered channel with a parked receiver the sender still returns immediately after handing over, and it does not wait for the receiver to actually run. - It is not a reason to pick unbuffered channels for performance. Pick capacity for the semantics you want — a rendezvous, or a bounded queue — and let the runtime pick the path. What the detail *is* good for is reasoning: once you know a send checks the waiter queue before the buffer, the wake-up rules, the FIFO ordering of a full buffered channel, and the memory-model guarantee all fall out of one picture instead of three separate rules.
- Does a send to a buffered channel with a parked receiver use the buffer?No. A receiver can only be parked on a channel whose buffer is empty, so the send takes the direct hand-off path: it copies into the receiver's destination and readies it, leaving the buffer untouched. Routing through the buffer would cost a second copy for no benefit.
- A buffered channel is full with senders parked on it, and a receive happens. What order do values come out in?Send order. The receive takes the head of the buffer — the oldest value — then moves the parked sender's newer value into the slot that just freed at the tail and readies that sender. So FIFO holds across the buffer and the waiting senders together.
- Why does the runtime need a write barrier for that copy?Because the sender is writing into another goroutine's stack, and the collector may already have scanned that stack. The barrier makes a pointer published this way visible to the garbage collector, so a freshly handed-over object cannot be treated as unreachable.
saying these in an interview costs you the question
- Says every send stores into the channel and the receiver reads it back
- Claims the receiver copies the value out after it wakes up
- Thinks a buffered channel always routes values through its buffer
- Says the receiver ends up sharing the sender's variable, not a copy
- Calls unbuffered channels inherently slower than buffered ones