DRAFT · needs Kron's war stories + a canonical part-name · not yet in the series index
Both languages come from the same place: cut the ways to be wrong. Go cut the language until there was less to argue about. Rust cut the class of memory and data-race bugs until the compiler stopped letting them compile. Same instinct, different layer. Go removes decisions at the keyboard. Rust removes them at the type checker.
So the question is never "which is better." It is "which class of bug am I paying to remove, and can I afford the price." Go's price is a garbage collector and a runtime. Rust's price is the borrow checker and the compile time. You buy one to dodge the other.
Go wins when the bottleneck is you, not the machine. Most services are. The model is small: one goroutine owns a piece of state, a channel hands it to the next, the GC cleans up behind you. You do not think about lifetimes. You do not annotate who owns what. You write the obvious thing and it works.
The single-goroutine-owns-state rule from Kron's
Go skill
is the whole game here. No shared mutable state means no lock ordering,
no data race to reason about, no Arc<Mutex> tax. The
goroutine that holds the value is the only one that touches it. Ownership
lives in your head as a rule, not in the type system as a proof.
// one goroutine owns cache. others talk to it, never touch it.
func cacheWorker(reqs <-chan req) {
cache := map[string][]byte{} // owned here, nowhere else
for r := range reqs {
r.reply <- cache[r.key] // hand a copy out, keep the map
}
}
No borrow checker needed to prove that map has one writer. The channel is the proof. That is Go's trade: you give up the compile-time guarantee and you get code you can write in an afternoon and read in a minute.
Go also wins on onboarding. The language is small enough to hold whole. A new hand reads the standard library and is productive in days. Rust's learning curve is real and it is front-loaded. If the team turns over, or the code must be legible to people who did not write it, Go's smallness is a feature you feel every week.
And it wins on compile time. Go builds in seconds. That speed is not a nicety; it changes how you work. You run the tests, you see the result, you move. Rust's compile can break the flow on a large crate. The fast loop keeps you in the problem instead of waiting on the machine.
Rust wins when the machine is the bottleneck and the cost of a bug is high. Three places it earns its price:
Latency floors. A GC pause is a tail-latency event you do not schedule. Most services shrug it off. A matching engine, an audio path, a market-data feed does not. Rust has no GC, so there is no pause to tune around. If your p99.9 is a hard number a customer sees, the absence of the collector is worth the borrow checker.
Embedded and no-runtime. Go ships a runtime and a scheduler. On a microcontroller with kilobytes of RAM, or in a kernel module, or a WASM target where every byte counts, that runtime is a cost you cannot pay. Rust runs with no GC and no scheduler. It goes where Go structurally cannot.
Correctness-critical code. When a data race or a use-after-free is not a bug ticket but a security hole or a corrupted ledger, the compiler refusing to build the racy version is worth more than any test. Rust moves that whole class of failure from runtime to compile time. You pay the borrow checker once, up front, and the bug never ships.
Owns state per goroutine. GC. Fast compile. Small language. Ship fast, read fast, onboard fast. Pays with a runtime and GC pauses.
Owns state in the type system. No GC, no runtime. Hard latency floor. Compiler refuses the race. Pays with borrow checker and compile time.
Here is the part that matters for this series. When the constraint pushes you to Rust, do not throw out the Go discipline. Carry it over. Kron's Go skill is a lint checklist, and most of it is language-agnostic:
| Go rule | What it means in Rust |
|---|---|
| single-goroutine-owns-state | One owner per value. Prefer message passing (mpsc) to Arc<Mutex>. The borrow checker rewards you for it. |
| parse-at-boundary | Validate at the edge, hold typed values inside. Rust's type system makes this cheaper, not harder. |
| full-word naming | No cryptic abbreviations. request, not rq. The reader is the constraint in both languages. |
*_test.go table tests | Table-driven tests port straight across. A slice of cases, one loop, clear failure output. |
The single-owner rule is the tell. In Go it is a convention you hold in your head. In Rust the borrow checker enforces the same shape by force. If you already write Go the disciplined way, Rust's ownership feels less like a wall and more like a compiler agreeing with a rule you already kept. Message passing over shared locks is idiomatic in both. The discipline travels.
Rust does not free you from the Go principles. It makes them mandatory. Write Rust like disciplined Go and the borrow checker stops being your enemy.
Default to Go. It is the shorter path from idea to shipped, and shipped is where you learn what the program should have been. Reach for Rust when a constraint you cannot negotiate demands it: a latency floor a customer feels, a target with no room for a runtime, a bug class that must not ship. Those are the cases. Outside them, the GC and the fast compile are not a weakness. They are the accelerant.
Same instinct under both: cut the ways to be wrong. Go cuts the language. Rust cuts the memory bugs. Pick the cut your constraint pays for, and carry the discipline either way. Light accelerates faster.