Every few years, the programming world collectively decides C is dead. The eulogy writes itself: no memory safety, no built-in concurrency, no ergonomic abstractions. Go swooped in with goroutines and channels, and suddenly everyone who’d ever wrestled with pthreads felt seen. The narrative was clean, satisfying, and wrong.
Here’s what nobody told you: Go didn’t invent concurrency. It just made it polite. The goroutine-channel model — lightweight threads communicating through typed pipes — is a design pattern, not a language feature. And patterns can be implemented anywhere. Even in a language from 1972.
Anton Zhiyanov’s recent work on Go-flavored concurrency in C isn’t a parlor trick. It’s a gut punch to anyone who assumed you need a garbage collector and a runtime scheduler to do ergonomic concurrency. Using user-space coroutines and message passing, C can emulate the goroutine-channel dance — and in specific, measurable scenarios, do it with less overhead than Go itself.
That sounds insane until you think about it. Go’s runtime is a magnificent piece of engineering, but it’s still a runtime. It has a garbage collector breathing down your neck, a scheduler making decisions you can’t override, and a stack model that trades memory for convenience. C has none of that. C has you, a stack pointer, and the cold freedom of doing whatever you want.
Every abstraction you add is a tax you pay at runtime. Go’s tax is hidden in the bill. C’s tax is on the menu.
The implementation is surprisingly elegant. User-space coroutines give you the lightweight scheduling of goroutines — context switches without kernel involvement, stacks that grow on demand. Channels become simple structures passing messages between coroutine contexts. The syntax isn’t as clean as Go’s go func(), but the semantics are there. You get the concurrency model without the runtime baggage.
Now, the honest part. This comes with trade-offs that would make a Go developer break out in hives. There’s no built-in safety net. No race detector watching your back. No garbage collector cleaning up after your mistakes. You’re implementing concurrency primitives in a language where buffer overflows are a Tuesday. The moment you get a pointer wrong in a coroutine, you’re in undefined behavior territory, and the compiler won’t save you.
But that’s the point. This isn’t about C being “better” than Go. It’s about exposing the lie that concurrency ergonomics require a managed runtime. They don’t. They require discipline, and C programmers have that in spades.
C doesn’t protect you from yourself — and that’s exactly why it can run circles around languages that do.
The deeper lesson here isn’t really about C or Go. It’s about how we think about language features versus design patterns. We’ve been trained to conflate the two. “Goroutines” feel like a language feature because Go baked them in. But the underlying idea — communicate by sharing channels, not by sharing memory — is architecture. You can build it in any language that gives you enough control over execution flow.
What C’s implementation reveals is the cost of Go’s convenience. When you strip away the runtime, you see exactly what goroutines are: coroutines with a scheduler, channels with type safety, and a garbage collector that makes the whole thing feel effortless. Each of those layers adds weight. In C, you get to choose which layers you want. In Go, you get all of them, always, whether you need them or not.
For embedded systems, game engines, and performance-critical infrastructure, that choice matters. These are domains where Go’s garbage collector pauses are unacceptable, where every kilobyte of runtime overhead is scrutinized, and where developers already live in C. Telling them to “just use Go” is telling them to abandon decades of tooling, libraries, and hard-won expertise for a concurrency model they can build themselves.
The response from the Go camp is predictable: “Why would you reinvent the wheel?” But that misses something fundamental about why C developers do this. They’re not reinventing the wheel. They’re machining a lighter one from titanium because the rubber one won’t survive the track.
The best concurrency model isn’t the one with the most features. It’s the one with the fewest compromises for your specific problem.
Zhiyanov’s work is a reminder that C isn’t a relic waiting to be replaced. It’s a living language that evolves not through committee specs and runtime updates, but through developers who refuse to accept that “modern” must mean “managed.” Every time someone implements a contemporary pattern in C — whether it’s goroutine-style concurrency, lambda-like closures, or actor-model message passing — they’re proving that the language’s minimalism isn’t a limitation. It’s a license.
So the next time someone tells you C can’t do modern concurrency, smile. You know something they don’t.
FAQ
Q: Isn't this just reinventing goroutines badly in a language without safety?
A: No. It's implementing the concurrency pattern without the runtime tax. Go's goroutines are coroutines plus a scheduler plus a garbage collector plus type-safe channels. C's version lets you pick which of those layers you actually need. You lose safety nets, but you gain control and lower overhead. Different trade-off, not worse one.
Q: When would I actually use this instead of just using Go?
A: Embedded systems, game engines, real-time infrastructure, and any domain where GC pauses or runtime overhead are non-negotiable dealbreakers. If you're building a web API, use Go. If you're building a game engine's job system, C's coroutine approach gives you the concurrency model without the baggage.
Q: Does this mean Go's concurrency model is overrated?
A: Not overrated — over-credited. Go made the CSP concurrency pattern accessible and safe, and that's genuinely valuable. But the pattern itself is language-agnostic. What C's implementation exposes is how much of Go's "magic" is really just runtime scaffolding you may not need.