← back to writing

Rust, Go, and the distribution-vs-guarantees matrix

When Rust is the right choice, when Go is, and why the honest answer is "it depends on which problem you're solving"

I ship open-source tools in both Rust and Go, and sometimes I port between them in both directions. That occasionally gets read as indecision. It isn’t. It’s a heuristic, and the heuristic is worth writing down.

The short version: Rust when the guarantees buy something real, Go when the distribution story dominates. Everything else is a footnote.

What “guarantees” means here

Rust’s guarantees (memory safety, thread safety, the absence of a GC pause, the type system’s handle on ownership) cost something at design time. You pay for them with borrow-checker time, with more explicit error handling, and with refactors that are sometimes harder than they would be in Go.

The question I ask before starting a project is whether the guarantees buy me anything the project actually needs. Concretely, they buy me:

  • FFI safety when the project crosses a C ABI boundary with known footguns. This is why quack-rs is Rust. The C extension API of DuckDB has a set of documented pitfalls, and Rust’s type system can encode them away.
  • Resident memory footprint when the project will sit on a tiny device. This is why BirdNet-Behavior is Rust. 20 MB versus 400 MB is the difference between running on a Pi Zero and not running at all. That is a property of no-GC languages with disciplined allocators, and Rust is the most approachable one.
  • Concurrency correctness when the project is doing something non-trivial with shared state. The compiler is more reliable than my reading of the code.
  • Long-term auditability when the project is going into a regulated environment. “Zero unsafe as a CI gate” is a real property.

If the project doesn’t benefit from any of those, Rust’s costs aren’t paying for anything. You’re just making your life harder.

What “distribution dominates” means

The other side of the heuristic: sometimes the hardest problem in the project isn’t making it correct. It’s getting it onto a hundred field devices in a format the operator can actually handle.

Go wins that brief comfortably. Static binary. Cross-compile to any target in seconds. No runtime to install. Predictable build times. And, crucially, a code style simple enough that someone reviewing the diff on a sleepy Tuesday afternoon is going to get it right. When the hard part of the project is “ship the thing, not write the thing,” Go is underrated.

lyrebirdaudio-go is the clearest example I have. The original LyreBirdAudio is a shell-script bundle, and the operators who deploy it asked for a single binary. The binary’s job is subprocess orchestration; it runs arecord and mediamtx and writes udev rules. There is essentially no computation happening inside the binary itself. Rewriting it in Rust would have been five times the work for zero benefit. The binary never held a hot loop where Rust’s guarantees would matter, and the simplicity of Go’s build and distribution model was exactly what the operators needed.

The port-in-both-directions thing

Sometimes I port a Rust project to Go, sometimes a Go project to Rust, sometimes a shell script to both. That isn’t thrashing; it’s testing the heuristic.

The port tells you quickly whether the guarantees-vs-distribution trade-off actually lands where you thought it would. If the Go version of a Rust project turns out to be indistinguishable in production, the Rust version didn’t need to be Rust. If it doesn’t (if the port reveals a performance cliff or a concurrency bug the compiler had been catching for you) you know the Rust version was earning its keep.

Doing the port in public also keeps the discipline honest. It’s very easy to fall in love with a language. The port is the evidence, one way or the other.

The uncomfortable corollary

The corollary of the heuristic is that most projects don’t need Rust. Most small tools, most internal services, and most glue code at most companies will not get anything from Rust’s guarantees that they couldn’t get from Go, or from a careful Python codebase with good tests. Rust is for the places where the guarantees change the shape of what you can ship. Everywhere else, the distribution-and-simplicity argument wins.

That’s uncomfortable to say if your identity is “Rust person.” It is also, in my experience, the only rule that survives contact with actually shipping software to people who didn’t write it.