skip to content

What does Go's runtime store on a channel so it knows which goroutine to wake?

level: middleimportance: should knowfreq 34%

answer

  1. a per-operation record, not just a goroutine pointer
  2. it must remember where the value goes
  3. two FIFO queues hang off every channel
  4. one goroutine in select needs several records
  5. the counterpart pops it and readies the goroutine

basics

~20 s

Go's runtime queues a wait record, called a sudog, for each blocked goroutine: it names the goroutine and the address the value goes to. A counterpart operation pops the first record from that channel's FIFO queue and readies its goroutine.

solid answer

~50 s

Every channel carries two intrusive FIFO queues — a receive queue and a send queue — of **wait records** the runtime calls `sudog`s. When a receive cannot proceed, the runtime allocates a sudog holding a pointer to the goroutine, a pointer to the destination the value should land in, and queue links, pushes it on the channel's receive queue, and parks the goroutine. A later send takes the channel's lock, pops the first sudog, completes the copy through the pointer it carries, and calls `goready` on that goroutine, which marks it runnable and hands it to a run queue. The queues are FIFO, so the longest-waiting goroutine is served first. The record exists separately from the goroutine because it also carries *where* the value goes, and because one goroutine in a `select` is enqueued on several channels at once with one record each.

go deeper

for a junior

You are not expected to name runtime internals here. Know the shape: a blocked goroutine leaves a record on the channel, and the goroutine on the other side is the one that wakes it.

for a middle

Describe the record and the two FIFO queues on the channel, what the record must remember besides the goroutine, and that the counterpart completes the copy and readies the waiter under the channel's lock.

for a senior

Draw out the consequences: wakes are caused by a counterpart and never discovered by a scanner, so a queue entry with no counterpart is permanent, and the parked goroutine's stack stays a GC root the whole time.

for a principal

Use this as a guardrail when reviewing designs. Any channel hand-off needs a named owner for the counterpart operation and a cancellation path, because the runtime provides no timeout and no supervisor for a queued waiter.

## The problem the wait record solves When a goroutine parks on a channel, two things must be remembered so the operation can be finished later by somebody else: 1. **which goroutine** to make runnable again, and 2. **where the value goes** — the address of the variable a receiver wants filled, or the address of the value a sender wants taken. A bare pointer to the goroutine would only answer the first. So the runtime allocates a small structure per waiting operation — internally named `sudog`, for "pseudo-g" — and queues it on the channel. ## What is in a sudog and where it lives A sudog carries roughly: a pointer to the waiting goroutine, an `elem` pointer to the value's source or destination, forward/backward links for the queue, the channel it is queued on, and a flag/ticket used for fairness and for `select` arbitration. A channel — the runtime's `hchan` — holds a mutex, the ring buffer and its indices (empty for an unbuffered channel), a closed flag, and the two waiter queues, conventionally `recvq` and `sendq`. Every operation on a channel takes the channel's lock first. That lock is what makes the whole park-or-proceed decision atomic: under it, a receiver either finds a waiting sender or a buffered value and proceeds, or enqueues its sudog and parks. There is no window in which a value arrives and nobody notices it. Sudogs are pooled: they are cached per scheduling context and refilled from a central free list, so the common park/unpark cycle does not allocate on the heap. ## Wake-up: goready The counterpart operation does the work on behalf of the parked goroutine. A send that finds a queued receiver pops that receiver's sudog, copies the value through the sudog's `elem` pointer, and calls `goready`, which: - moves the goroutine from *waiting* to *runnable*; - puts it on the current scheduling context's run queue — into the fast "run this next" slot, so a goroutine you just handed a value to tends to run very soon on the same thread, which is good for cache locality; - may wake an idle thread if there is spare parallelism to exploit. Note what this implies: **the waking is done by the sender's goroutine, synchronously, inside the send.** Nothing in the runtime scans channels looking for satisfiable waiters. ## Ordering guarantees Both queues are FIFO. If three goroutines are parked receiving on the same channel and one value is sent, the goroutine that has been waiting longest gets it. This is a real fairness property people rely on when a channel is used as a hand-off point, and it is worth stating precisely because the randomised choice people remember from `select` is a *different* mechanism: a `select` picks randomly among cases that are **ready at that moment**, while a channel's waiter queue serves parked goroutines in arrival order. ## One goroutine, many records A goroutine blocked in a `select` over three channels is enqueued on all three: one sudog per case, each pointing back at the same goroutine. Whichever channel fires first must claim the goroutine — that is what the ticket/flag in the sudog is for — and then dequeue the other records so a second channel cannot also try to wake it. This is the clearest reason the wait record cannot simply be the goroutine itself: a single goroutine can appear in many queues at once, at different positions, with different destination pointers. Mutexes and wait groups use the same machinery from the other end: they park with a different wait reason and are readied through the runtime's semaphore tables rather than a channel's queues, but the park/ready pair is identical. ## Why this matters in practice Three practical consequences fall out of the design: - **A wake is caused, never discovered.** If no goroutine ever performs the counterpart operation, the sudog sits in the queue forever and the goroutine never runs again. Nothing in the runtime times it out. - **The queue is part of the channel's liveness.** A parked goroutine keeps its stack alive as a GC root, and the sudog keeps the goroutine reachable, so "nobody references the channel any more" does not release anybody. - **The hand-off is cheap.** Because the sudog already names the destination, the counterpart can finish the transfer with one copy and one ready call while holding the channel lock, rather than storing the value somewhere for the sleeping goroutine to pick up later. You will not name `sudog` in day-to-day Go, but knowing that a park leaves a *record on the channel* rather than a *note in the scheduler* is what makes the wake-up rules — FIFO order, no timeouts, wake-by-counterpart — obvious instead of memorised.

  • Why isn't a pointer to the blocked goroutine enough?
    Because the record also carries the address the value must be copied to or from, so the counterpart can finish the transfer without the sleeping goroutine's help. And one goroutine blocked in a `select` is queued on several channels at once, each with its own destination and queue position, which a single embedded field on the goroutine could not represent.
  • If three goroutines are parked receiving on one channel and one value is sent, which one gets it?
    The one that parked first. A channel's waiter queues are FIFO, so the value goes to the longest-waiting receiver and only that goroutine is readied; the other two stay parked. The randomised choice people associate with `select` applies to cases that are ready simultaneously, not to the order of a channel's waiter queue.
  • What does goready do beyond flipping the goroutine's state?
    It places the newly runnable goroutine on the current scheduling context's run queue — specifically in the fast next-to-run slot — so it tends to run immediately after the waker on the same thread, which keeps the transferred data cache-warm. It may also wake an idle thread so the woken goroutine can run in parallel.

saying these in an interview costs you the question

  • Says the scheduler scans channels looking for goroutines it can wake
  • Thinks the parked goroutine copies the value itself after waking
  • Claims waiters on a channel are woken in random order
  • Believes a blocked select registers on only one channel at a time
  • Assumes each park allocates a fresh heap object every time