← All posts
6 min read

Is Go a Better C? An Honest Look at a Tempting Claim

A project called Solod made the rounds recently — a strict subset of Go that transpiles to readable C11, pitched under the headline "Go can be a better C." It drew a big, argumentative Hacker News thread, which is usually a sign the claim touches a nerve.

I'm a backend developer. I live in Python and TypeScript, and I've written enough C++, Java, and Kotlin to respect what a runtime does for you and what it costs. I'm not a systems programmer, so treat this as analysis, not a war story. But "language X is a better C" is a claim you can actually evaluate, so let's do that honestly — both where Go competes and where it plainly doesn't.

What Solod is actually proposing

Solod ("So") is not Go-the-runtime. It's a strict subset of Go's syntax that compiles to C. The pitch, from the project's own writeup:

  • Zero runtime. No garbage collection, no reference counting, no hidden allocations.
  • Stack by default. Everything is stack-allocated unless you opt into the heap through the standard library.
  • Native C interop with "no CGO, no overhead" — strings and slices convert to their C equivalents automatically.
  • Go tooling for free — because the source is valid Go, you get LSP, linting, and go test.

Performance-wise the author is measured about it: Solod is "usually on par with Go or faster," while conceding hand-written C can still win. As of July 2026 it's at v0.2 (networking, WebAssembly, freestanding mode), and the author is explicit that it's not for production.

Notice what happened there. To make Go a "better C," Solod had to throw away the garbage collector, the goroutine scheduler, and the runtime — the exact things that make Go Go. That tension is the whole debate.

Where Go genuinely competes with C

This part is real, and worth saying clearly.

Memory safety. Plain C hands you buffer overflows, use-after-free, and dangling pointers as a default. Go's model — bounds-checked slices, no pointer arithmetic, a runtime that panics instead of corrupting memory — eliminates entire bug classes that have caused decades of CVEs. For the vast majority of software, one HN commenter's framing lands: manual memory management is "a liability rather than an asset."

Concurrency. Goroutines and channels are a genuinely better default than raw pthreads. Cheap green threads plus a scheduler is a real ergonomic and safety win over hand-rolled thread pools and mutex soup.

Tooling and build. One static binary, cross-compilation as a flag, go test, gofmt, a module system. C's story here is a patchwork of Make, CMake, autotools, and whatever your distro packaged. Go's is boring in the good way.

Simplicity. Go's designers came out of C and Plan 9 (Thompson, Pike, Griesemer). The language is deliberately small. For network services and CLIs — the stuff C historically got used for and shouldn't have — Go is a strictly nicer place to work.

So for the upper layer of "systems programming" — daemons, proxies, container tooling, network services — Go already won, and it won years ago. Docker, Kubernetes, and most of the cloud-native stack are Go, not C.

Where it does not compete

The claim breaks down the moment you go below that layer, and it breaks down because of the runtime.

The garbage collector. Go's GC is genuinely good: concurrent, non-moving, tri-color mark-sweep, tuned for low latency rather than throughput. It's not fully stop-the-world — most work runs concurrently, and the brief STW pauses at phase transitions aren't proportional to heap size. But "brief" is not "none." The mark phase can claim ~25% of CPU, your own goroutines get drafted to help collect under allocation pressure, and you tune it with blunt knobs: GOGC (default 100) and GOMEMLIMIT (since Go 1.19). That's fine for a P99 latency budget measured in milliseconds. It's disqualifying for hard-realtime, where a pause you can't bound is a missed deadline, full stop.

No manual memory control. You can't write a custom arena allocator, pool memory by hand, or guarantee zero allocation on a hot path the way C (or Rust, or Zig) lets you. unsafe and cgo exist as escape hatches, but cgo isn't free — crossing the boundary has real call overhead and defeats a lot of Go's tooling.

The runtime and binary size. Every Go binary carries the scheduler and GC with it. That's why "hello world" is measured in megabytes, not kilobytes — a real problem when you're targeting a microcontroller with tens of kilobytes of RAM.

Embedded is a different planet. TinyGo exists and is impressive, but it's an honest illustration of the ceiling. Its GC is a conservative mark-sweep collector borrowed from MicroPython — it scans memory for anything that looks like a pointer, can't move objects, and therefore can't compact, so you can hit out-of-memory from fragmentation with free memory still on the heap. It's "a lot slower than the usual Go GC," has visible pauses, and degrades on tiny AVR chips and WebAssembly. Some Go semantics get restricted too — for example, recovering from runtime panics like nil dereference isn't supported everywhere. None of that is a knock on TinyGo; it's what happens when you drag a GC'd language down to the metal.

This is why Solod deletes the runtime instead of shipping it. And once you've deleted it, the fairer question is whether you're still writing Go — or just C with Go's syntax and, as HN critics noted, none of Go's actual safety guarantees. One commenter pointed out you can still return a dangling pointer; the transpiler won't save you.

The scorecard

Here's how I'd line it up for genuinely low-level work. This is my read, not gospel:

Concern C Go Rust
Memory safety by default No Yes (GC) Yes (ownership)
Manual memory control Full No (GC-managed) Full
Predictable/realtime latency Yes No (GC pauses) Yes
Runtime & binary size Minimal Heavy (GC + scheduler) Minimal
Concurrency ergonomics Poor Excellent (goroutines) Good (fearless, harder)
Embedded / bare metal First-class Limited (TinyGo) Strong & growing
Tooling & build Fragmented Excellent Excellent
Learning curve Moderate Low Steep

My take

The honest verdict: "Go is a better C" is true for the top half of systems programming and false for the bottom half — and the bottom half is where "systems" usually means something.

For network daemons, infrastructure tooling, and services, Go beat C on ergonomics, safety, and tooling long ago, and I'd reach for it happily. But for kernels, drivers, allocators, hard-realtime, and constrained embedded, the GC and runtime are non-negotiable disqualifiers — and the language competing for C's actual crown right now is Rust, which gives you memory safety and manual control without a collector. If I wanted C's performance envelope with modern safety, that's where I'd look before a Go-to-C transpiler.

Solod itself is a genuinely clever hack, and I like it as an argument-made-runnable about which parts of Go are essence and which are runtime. But it proves the opposite of its headline: to make Go a better C, you have to stop it from being Go.

Sources