Why Go for the service, Rust for the tool
Part of “Why this over that”: short posts about technology choices I’ve actually made, with the reversal conditions stated honestly.
I write Go and Rust every week, and I hold both without irony: Aevu’s API is Go, my day job’s telemetry collector is Go; meanwhile my toolchain manager, my Postgres wire-protocol work, and the Tracegres collector are Rust. That split wasn’t a plan. It emerged from repetition, and once I noticed it, the pattern explained itself.
Go for the service. Rust for the tool. Not because either language “wins,” but because services and tools are different artifacts with different failure economics.
What a service actually needs
A network service is mostly plumbing under churn: parse a request, check permissions, hit a database, emit JSON, log it. The dominant costs are iteration speed and operational legibility, because the code changes weekly and is read by whoever is on call.
Go is built for exactly this shape. The language is small enough that anyone’s code reads like everyone’s; goroutines make “handle ten thousand connections politely” the default rather than an achievement; the GC is a non-issue for IO-bound work where every request already includes a database round trip; deployment is one static binary; and compile times keep the edit-run loop tight. Go’s ceiling on expressiveness is real, and for services it’s a feature: there are fewer clever ways to write it, so there are fewer clever ways to misread it at 3 a.m.
What a tool actually needs
A tool (a CLI, a collector, a parser, a protocol decoder) has opposite economics. It ships to machines you don’t control, where “restart it” isn’t your call. It sits in hot paths where its overhead is someone else’s budget. And it’s finished in a way services never are: correctness compounds, churn is low, and the type system’s upfront tax amortises over years.
That’s Rust’s home ground. No GC pauses in a collector that must never be the noisy neighbour on someone’s production box. Enums and exhaustive matching are exactly the right shape for wire protocols and state machines, where “I forgot a message type” becomes a compile error instead of a field incident. clippy and the borrow checker are a code review that never sleeps. When decoding PostgreSQL traffic or gating releases on plan comparisons, “if it compiles it’s probably correct” is not a meme, it’s throughput.
The honest reversals
- Rust for the service when the service is the hot path: a proxy, a query engine, an ingest tier where GC tail latency is the product. Then the service has tool economics, and Rust earns its keep. (Databases are the canonical example, which is why my database-shaped projects are Rust even when they serve networks.)
- Go for the tool when the tool is glue with a deadline: an internal CLI wrapping some APIs, a migration script with a long life. Tool in shape, service in economics; Go ships it this week.
- Team gravity beats theory. A Rust shop should write Rust services; a Go shop should write Go tools. Consistency compounds harder than either language’s advantages. This rule applies to greenfield choices, not to conversions.
The takeaway
Stop asking “which language is better” and ask “what are this artifact’s failure economics?” Code that changes weekly and fails loudly on your own servers wants Go’s legibility. Code that changes rarely and fails quietly on other people’s machines wants Rust’s guarantees. The languages aren’t rivals; they’re specialists, and the novelty budget is better spent on your product than on forcing one of them to do the other’s job.
Also in this series: Why SQLite over Postgres, Why SSE over WebSockets, and Why markdown files over a CMS.
No account, no tracking. One vote per reader.