iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
When code writes ch <- value or value = <-ch, the Go runtime does not simply push the value into a queue. It first checks whether a partner goroutine is already waiting and can take the value directly. If not, it copies the value into a circular buffer when the channel has one with free space. If neither works, it parks the calling goroutine on a wait queue until a partner arrives. Closing a channel wakes the goroutines still waiting on it. All of this logic sits in one file, runtime/chan.go, and this article walks through what that file shows and where its evidence stops.
Where the logic lives and which version this covers
Channel operations are implemented in go.dev/src/runtime/chan.go. The runtime implementation of select is in go.dev/src/runtime/select.go. The material behind this article was read from the official source views of both files as they stood on 7 October 2026. That view did not show a Go release tag or a commit hash, so the details below describe that view of the code rather than a guaranteed behaviour of any specific Go release. If you need to rely on an exact version, open the source at your release tag and check the same functions, because names and control flow can change between releases. The file also contains no benchmark figures, so nothing here says anything about speed.
Start with the syntax the runtime serves
An official Go presentation dated 18 October 2010 gives the two basic forms: a send is written ch <- value, where the arrow points in the direction the value flows, and a receive is written value = <-ch. The same presentation states that “Channels are unbuffered by default.” Everything that follows is what the runtime does behind those two expressions.
The channel object: hchan
A channel value points to a struct named hchan. The fields, as named in the reviewed file, track:
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallqcountanddataqsiz: how many elements are currently buffered and the buffer’s capacity.- a buffer pointer: the storage for buffered elements.
- element size and element type: the metadata used when values are copied.
- a close flag: whether the channel has been closed.
- send and receive indices: the positions used to move through the circular buffer.
recvqandsendq: the wait queues of blocked receivers and blocked senders.- a lock: the mutex that protects the channel’s fields.
Blocked goroutines are represented by sudog records that sit in those queues. The lock comment in the file says it also protects several fields in these blocked records, so the queues and the channel state change together under one lock.
#1 Best Overall
Invariants the file relies on
The comments in hchan describe state relationships that the operations are meant to keep true:
- Ordinarily at least one of the send queue and the receive queue is empty. A channel with a waiting sender and a waiting receiver at the same time would mean they should have met already.
- The documented exception is an unbuffered channel where a single goroutine is blocked on both a send and a receive through
select. - For a buffered channel, queued data implies there is no waiting receiver, and unused buffer capacity implies there is no waiting sender.
These are internal invariants described by the file’s comments. They are not a promise that every operation passes through one simple queue, and they should not be read as a public API guarantee.
The send path, in order
In the reviewed view, a send follows this sequence:
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors- Check for a closed channel. A send on a closed channel panics.
- Look for a waiting receiver. If one is parked in
recvq, the value is handed to it directly. The buffer is bypassed entirely. - Look for free buffer space. If the channel has spare capacity, the value is copied into the circular buffer at the send index, and the index advances.
- Block. If there is no receiver and no free space, the runtime records the sender as a
sudoginsendqand parks the goroutine.
This is why a channel is more than a first-in, first-out buffer. It can coordinate a direct handoff between two goroutines, and it keeps wait queues for operations that cannot yet proceed.
The receive path, in order
The receive side mirrors the send side, with a few cases of its own:
- Pair with a waiting sender on an unbuffered channel. The receiver copies the value directly from the waiting sender.
- Handle a full buffered channel with a waiting sender. The receiver removes the oldest buffered element and moves the sender’s value into the newly freed tail slot. The buffer keeps its order, and the blocked sender is released.
- Take buffered data. If the buffer holds values and no sender is waiting, the oldest value is taken.
- Handle a closed, drained channel. The receive returns the element type’s zero value and reports that no value was received.
- Block. Otherwise the receiver is recorded in
recvqand parked.
The two-result form, v, ok := <-ch, makes the difference visible. When ok is false, the channel supplied no value. That is different from receiving a value that happens to equal the zero value of the type, such as 0 for an int that was actually sent. Only the boolean distinguishes those two cases.
Closing a channel
closechan panics in two cases: when the channel is nil, and when it is already closed. Otherwise it takes the channel lock, marks the channel closed, and removes every blocked receiver and sender from the queues. It releases them after dropping the lock, which means the wake-up work happens outside the critical section.
Rank #4
- A blocked receiver is released and receives the zero value, with
okset tofalse. - A blocked sender is released into the send-on-closed-channel panic path.
- Values already sitting in the buffer stay available to receivers. The “closed and drained” behaviour applies only after the buffer empties.
Unbuffered and buffered channels compared
The difference between the two kinds of channel comes down to whether there is a buffer for values to wait in. The table below follows the send and receive paths described above.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
| Aspect | Unbuffered channel | Buffered channel |
|---|---|---|
| Queue capacity | No buffer slot | A circular buffer with capacity dataqsiz |
| Core model | A sender and a receiver must meet (rendezvous) | A send can finish without a receiver if there is free space |
| When a send blocks | When no receiver is waiting | When no receiver is waiting and the buffer is full |
| When a receive blocks | When no sender is waiting | When no sender is waiting and the buffer is empty |
| How the value moves | Directly from one goroutine to the other | Directly to a waiting receiver, otherwise through the circular buffer |
| Default | Channels are unbuffered by default, per the official presentation of 18 October 2010 | Requires an explicit capacity |
How select fits in
select.go holds the runtime’s implementation of select, while chan.go documents the one queue exception that involves a goroutine blocked on both sides through select. If you are explaining how select coordinates with channels, read both files together. Reading chan.go alone shows the queue structure, but not the full selection logic.
Best Value
A reading path through the file
- Open go.dev/src/runtime/chan.go and find the
hchanstruct. Read its field comments and the invariant notes before anything else. - Find the send function,
chansend, and mark the four steps listed above. - Find the receive function,
chanrecv, and compare its branches with the send branches. - Find
closechanand trace which queues it empties and in what order it releases goroutines. - Open go.dev/src/runtime/select.go and locate the runtime’s
selectimplementation, then check how it touches the same queues.
Working through the file this way makes the behaviour of channels concrete: each operation either finds a partner, uses buffer space, or leaves a record in a queue and waits.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

