Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsA Go channel is more than a queue with a lock around it. In the runtime source, a channel is an hchan structure, and every send or receive comes down to three possibilities: a counterpart is already waiting, there is room in the buffer, or the goroutine must park until one of those becomes true. Reading runtime/chan.go makes that decision order concrete, and it explains why unbuffered and buffered channels behave so differently.
Where the code lives and which version it reflects
Two files matter for this topic. go.dev/src/runtime/chan.go holds the channel operations, and go.dev/src/runtime/select.go holds the runtime implementation of select. The source views used for this article were accessed on 7 October 2026. They do not identify a Go release tag or commit hash, so the line-level details below describe that view rather than a specific release. If you quote the code in production work, open the file at the Go version you run.
As an Amazon Associate I earn from qualifying purchases.
Start with the language contract
Go’s official presentation from 18 October 2010 gives the basic syntax: “Channel send (arrow points in direction of flow): ch <- value” and “Channel receive: value = <-ch“. It also states that “Channels are unbuffered by default.” Those three points define what the runtime must handle: a value passing between goroutines, a choice between direct handoff and buffered storage, and a default in which a send waits for a receiver.
The hchan structure
The source defines hchan with the following parts:
- Occupancy and capacity (
qcountanddataqsiz): how many elements are buffered now, and how many the buffer can hold. - Buffer pointer: the storage for the circular buffer.
- Element metadata: the element size and type, so values can be copied correctly.
- Closed state: whether the channel has been closed.
- Send and receive indices: the positions used to write to and read from the circular buffer.
- Send and receive wait queues: the blocked senders and blocked receivers, with the send queue appearing in the source as
sendq. - Lock: a mutex. The source comment says it protects the channel fields and several fields in the blocked
sudogrecords, which are the records that describe a parked goroutine.
Invariants that make the queues make sense
The source states that, ordinarily, at least one of the send and receive queues is empty. The documented exception is an unbuffered channel where a single goroutine is blocked on both a send and a receive through select. For buffered channels, two further rules follow: queued data means no receiver is waiting, and unused buffer capacity means no sender is waiting. These are internal invariants the code relies on. They are not a public promise about the order in which goroutines are served.
#1 Best Overall
What happens on a send
The send path checks conditions in a fixed order:
- Closed channel. The send is rejected and panics on the send-on-closed-channel path.
- Waiting receiver. If a receiver is already parked in the receive queue, the runtime passes the value directly to it. The buffer is bypassed.
- Free buffer space. Otherwise, if the buffer has spare capacity, the value is copied into the circular buffer at the send index, and the index advances.
- Park. If none of these routes is open and the send is blocking, the runtime records a
sudoginsendqand parks the goroutine.
This is why a channel is not simply a FIFO buffer. It also coordinates direct handoffs between goroutines and keeps wait queues for operations that cannot proceed yet. The exact scheduling and memory-management mechanics are implementation details of this source view, and they should not be generalized beyond it.
What happens on a receive
The receive path checks for a waiting sender and for buffered data:
- Unbuffered channel with a waiting sender. The value is copied directly from the sender.
- Full buffered channel with a waiting sender. The oldest buffered element is removed for the receiver, and the sender’s value is moved into the buffer slot that just became free at the tail.
- Buffered data with no waiting sender. The oldest buffered value is returned.
- Closed and drained. If the channel is closed and no buffered data remains, the receive returns the element type’s zero value and reports that no value was received.
The two-result form makes that last case visible in code:
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11v, ok := <-ch
if !ok {
// The channel is closed and drained; v is the zero value.
}
The boolean is the signal to rely on. A channel can legitimately carry the zero value of its element type, for example 0 for an int channel, so checking v == 0 cannot tell a real value from the closed-channel result. Checking ok can.
What closing does
- Nil channel.
closechanpanics. - Already closed. Closing again also panics.
- Normal close. The runtime locks the channel, marks it closed, and dequeues all blocked readers and writers. It releases them after dropping the lock.
- Blocked readers. They receive no value. They get the zero value with the “not received” indication.
- Blocked writers. They resume into the send-on-closed-channel panic path.
- Buffered values. Values already in the buffer remain available to receivers. The closed-and-drained behavior applies only after they are consumed.
Unbuffered and buffered channels side by side
The three axes that matter are queue capacity, when operations block, and whether values move directly between goroutines or through the circular buffer.
| Aspect | Unbuffered (make(chan T)) |
Buffered (make(chan T, n)) |
|---|---|---|
| Queue capacity | No buffer slot | n slots in a circular buffer |
| Send when a receiver is already waiting | Value passed directly to the receiver | Value passed directly to the receiver, bypassing the buffer |
| Send when no receiver is waiting | Blocks in the send queue until a receiver arrives | Copied into a free slot and the send completes; blocks only when the buffer is full |
| Receive when a sender is waiting | Copied directly from the sender | Oldest buffered value is taken; if the buffer is full, the sender’s value fills the freed slot |
| Receive when nothing is available | Blocks until a sender arrives or the channel closes | Blocks until a value is buffered, a sender arrives, or the channel closes |
| Where values travel | Directly between goroutines | Through the circular buffer when operations cannot pair directly |
Where select enters the picture
select has its own runtime implementation in runtime/select.go, but it interacts with channel queues, and chan.go documents the one queue-invariant exception tied to it. If you trace a select statement, follow it into select.go, then return to chan.go for the queue operations it shares with plain sends and receives.
Rank #4
Limits of this reading
- Version. The source views carry no release tag or commit, so the field names and steps describe that view. Check the file at your Go release before relying on a specific detail.
- Ordering. The reviewed code does not establish a guarantee about which waiting goroutine is woken first. Do not build program logic on an assumed wake-up order.
- Performance. The reviewed source contains no benchmarks or measured figures. Nothing here says how fast channel operations are compared with other mechanisms.
What the source does settle is the core mechanism: a send or receive either pairs with a waiting counterpart, uses buffer space, or parks, and close wakes everyone who is waiting.
Quick Recap
Best Value
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.




