Go Channels Are Handoffs, Not Queues
CsMadeEz's 14-minute Go tutorial reframes channels as synchronization handoffs—not queues—and the distinction has real consequences for concurrent program design.
Written by AI. Dev Kapoor

Photo: AI. Sela Marin
There's a specific kind of humbling that Go concurrency delivers. You write three lines. Every single line is documented, correct behavior. And the program deadlocks before it prints anything. This is how CsMadeEz opens a recent 14-minute video on Go channels—and it's a better hook than most conference talks manage, because the frustration it invokes is absolutely real.
The video's central argument is deceptively simple: the mental model most developers bring to channels is wrong, and that wrongness is the root cause of most channel-related bugs. The fix isn't more sophisticated code. It's a different concept.
The Queue That Isn't One
When you look at a channel syntactically—put a value in one end, get it out the other—your brain maps it to a queue. CsMadeEz admits this instinct directly: "I should confess something. When I was learning Go, people kept telling me, 'A channel isn't a queue.' I'd nod, go back to my editor, and completely ignore them." That's a useful admission, because it names the failure mode before explaining why it matters.
The actual model: a channel is a handoff. Two goroutines meet at a specific moment. One offers a value, the other takes it. Neither can continue until the other arrives. There's no buffer waiting in the middle. No hidden mailbox. For an unbuffered channel—Go's default—the runtime's internal hchan struct has a nil buffer pointer. There is, literally, nowhere for the value to park.
CsMadeEz uses a restaurant kitchen as the persistent metaphor: a steel pass-through counter where chefs hand plates directly to runners. The chef doesn't set the plate down and walk away. They hold it out. If no runner is there, they wait. If the runner arrives first, the runner waits. The plate goes directly from hands to hands.
This is more than a pedagogical convenience. It describes exactly what the Go runtime does under the hood. When a sender and receiver meet on an unbuffered channel, the value is copied directly from the sender's stack into the receiver's stack—the channel itself is never involved as storage. "One copy, nothing in between," as the video puts it. When nobody is waiting on the other side, the goroutine parks itself in a send or receive queue and the OS thread it was running on walks away to do something else. That's the same mechanism Go uses while waiting on a network socket.
This reframe has an immediate practical consequence for the three-line deadlock. Your single goroutine is both chef and runner. It walks up to the pass, holds out a plate, and waits for someone to take it—but the only goroutine that could take it is itself, and it can't get there until it lets go of the plate it's already holding. Go's runtime detects this, prints a fatal error, and exits. As CsMadeEz notes, this is actually a feature: plenty of other runtimes will let a deadlock sit there silently until someone eventually restarts the server.
What Buffering Actually Buys You (And What It Doesn't)
Buffered channels are where the video earns its nuance. Adding a buffer—a second argument to make—gives the channel a small shelf. The chef can set a plate down and go back to cooking without waiting for a runner. If the shelf is full, they wait. That's it. The video is emphatic that this does not make your program faster, and it does not make it safer.
What it does offer is breathing room between two goroutines running at slightly different speeds. If results arrive in bursts while a collector runs at a steady pace, a small buffer lets the burst land without forcing a synchronous stop-and-wait on every item. That's a legitimate use case.
The trap is treating a buffer as a bug fix. CsMadeEz describes the pattern precisely: program deadlocks, developer adds a buffer, deadlock disappears, developer ships it. "Except you didn't actually fix the problem. You just made the bug quiet enough to survive testing so it can come back later under real production load."
The deadlock was sending a signal. The producer was outrunning the consumer. Blocking the producer is the correct response—that's back pressure, and it's the difference between a system that degrades gracefully and one that silently eats memory until something falls over. Making the buffer arbitrarily large doesn't resolve the underlying mismatch; it just delays the reckoning while also, as the video drily observes, effectively turning your channel into a slice with extra steps and a lock.
Closing Is an Announcement, Not a Deletion
Roughly half the correctness problems with channels have nothing to do with sending and receiving—they have to do with knowing when to stop. The video's treatment of channel closing is one of its more careful sections.
Closing a channel is not cleanup. Nothing is freed, nothing is deleted. It is an announcement: no more values are coming. Anything already in the buffer still gets delivered in order. After that, receives return immediately with the zero value and a false boolean. A range loop over a channel runs this logic implicitly, stopping when the channel closes.
The ownership rule matters here: only the sender closes, never the receiver. The kitchen knows when it's done for the night; the runner standing at the pass does not. Violating this—having a receiver close a channel—risks panics when other senders try to send to an already-closed channel.
There's a subtler property that the video flags as "my favorite part": closing a channel is the only operation that every waiting goroutine sees simultaneously. A send wakes up exactly one receiver. A close wakes up all of them. This is how Go's context package actually works—the cancellation signal is just a channel that gets closed. When a request is cancelled, one close() call somewhere in the standard library wakes every goroutine waiting on that context at the same instant. Rob Pike's maxim—"Don't communicate by sharing memory. Share memory by communicating"—turns out to describe not just data flow but coordination itself.
Three Failure Modes Worth Naming
The video closes with three specific pitfalls, and they're worth laying out plainly.
Goroutine leaks. Go's fatal deadlock error only fires when every goroutine is asleep. If one goroutine gets stuck waiting on a channel while the rest of the program keeps running, you get no error at all. Just a worker that quietly stopped doing its job, potentially weeks ago. The discipline the video recommends: for every channel you create, you should be able to state out loud who closes it and when. If you can't answer that, the leak is probably already there.
Nil channels. A channel variable that's been declared but never initialized with make isn't an empty channel. It's nil, and operations on it block forever without panicking. There's no error. The code looks like it's running. This is one of the few places in Go where doing absolutely nothing is behaviorally indistinguishable from working correctly.
Channels where a mutex would have been cleaner. This one the video frames not as a bug but as a habit—and it's the most interesting entry on the list. The Go proverb the video quotes is precise: "Channels orchestrate, mutexes serialize." A shared counter updated by multiple goroutines doesn't need a channel; a mutex is three lines and every future reader understands immediately what's happening. The impulse to use channels for everything is well-intentioned—channels feel idiomatic, they feel Go-ish—but idiomatic isn't the same as appropriate.
This last point is worth sitting with a moment, because the video's title positions channels as the superior concurrency tool, and yet its own content argues for knowing which tool fits the problem. Go didn't eliminate the need for mutexes. It created a design space where, for many common concurrency patterns, you can structure your program so ownership is always clear and shared state is rarely necessary. That's a different claim, and a more defensible one.
The channel-as-handoff model is ultimately a model of ownership: a value is always in exactly one pair of hands. That clarity is what prevents data races, not anything magical about the channel mechanism itself. Whether that clarity is achieved with a channel, a mutex, or some combination of both is still a judgment call that lands on the programmer.
More Like This
How the Nest Thermostat Launched the Smart Home Era
Tony Fadell's Nest Learning Thermostat didn't just fix an ugly device—it sparked the smart home era. A look at what it got right, wrong, and what Google killed.
Async Rust Performance: What Most Developers Get Wrong
Code to the Moon breaks down async Rust and Tokio misconceptions that kill performance. Single-threaded concurrency vs parallelism explained.
Should You Learn C++ in 2026? The Uncomfortable Truth
C++ still powers billions of lines of production code, but newer languages promise better safety and tooling. What should developers actually learn?
Running Parallel AI Coding Agents Without Chaos
Running multiple Claude Code sessions in parallel creates real coordination problems. Here's the infrastructure stack that actually prevents them from breaking each other.
What malloc Actually Does (It's Not Magic)
Dave's Garage breaks down how malloc really works—from a five-line bump allocator to 40 years of fragmentation fixes, security patches, and thread nightmares.
Bridging the Gap: C++ Workshop Tackles Industry Reality
Amir Kirsh's workshop addresses the persistent divide between academic C++ and production code—and questions whether one-day training can solve it.
Alberto Brandolini on Managing Software Model Complexity
EventStorming creator Alberto Brandolini argues at GOTO 2025 that bounded contexts and visual maps are the antidote to software's inevitable drift toward chaos.
What Makes API Design an Art, Not a Science
Christoph Stiller's C++Online 2026 talk breaks down why good API design is a discipline in itself—and what separates craft from afterthought.