~/posts/why-i-left-go-for-rust

i didn't leave go for rust. i split my stack.

the 'which language' question is the wrong question. go for services, rust for tools — and here's the exact line.

“so are you a go guy or a rust guy now?”

somebody asked me that recently and i realised i’d been dodging it for the better part of a year. the honest answer is annoying, which is why nobody likes it: i’m both. and it’s not a fence i’m sitting on, it’s a split — i write go and i write rust, on purpose, for different jobs. the moment i stopped treating “which language” as a personality question and started treating it as a tooling question, the whole tired argument just dissolved.

the false binary

the internet wants this to be a war. go versus rust. gc versus the borrow checker. simple versus powerful. pick a tribe, get a flag, fight about it under someone’s benchmark tweet. and look — if you only ever build one kind of thing, you can absolutely get away with one language and a strong opinion. good for you.

but i looked at what i actually shipped this past year and the pattern is embarrassingly clean. it wasn’t a philosophy. it was a reflex, and the reflex was almost never wrong.

everything with users is in go

every backend i’ve written up on this blog is go. the dojo api . a pet-adoption thing . a nougat-voting toy that i somehow spent way too long hardening. all go, and not by accident.

go is boring in the best possible way, and for a service that is the entire point. there’s roughly one obvious way to do a thing, so i’m not burning a decision on which of five abstractions to reach for. goroutines make concurrency something i can actually read instead of a puzzle. it compiles in a blink, so the write-run-look loop stays tight while the product’s requirements are still thrashing around under me every sprint. and — the part i undersold to myself for years — someone else can read it. i can read it, at 2am, eight months later, with no context left in my head.

when the artifact is a business web service whose spec changes weekly, “boring, legible, fast to change” beats every clever type-system trick you can name. go gets out of the way. that’s not a language failing to be sophisticated. that’s the feature, working exactly as intended.

everything that’s a tool is in rust

then there’s the other pile. a log analyzer that mmaps files and borrows straight out of the buffer. three mcp servers talking json-rpc over stdio. charts rendered in the browser straight from wasm, no javascript library in sight. every one of them is rust, and again — not by accident.

here the code is the product. the spec is stable — a log format, a protocol, a rendering routine don’t renegotiate themselves every sprint the way a product feature does. correctness and performance aren’t nice-to-haves bolted on later; they’re the whole value proposition. and this is the exact spot where rust’s gifts stop being abstract and start paying rent:

  • control. zero-cost abstractions, no gc pause in the middle of a hot parse loop, borrowed slices instead of copies, memory i can reason about down to the byte. you cannot ship a garbage collector into a 30kb wasm bundle, and you shouldn’t want to.
  • correctness that the compiler enforces. this is the one that converted me. an error isn’t a value sitting next to val that i’m free to ignore — it’s in the type, and i don’t get the value without dealing with it:
go
// go: the error is right there. so is the temptation to just... not.
val, err := doThing()
if err != nil {
    return err
}
rust
// rust: no val until you've handled the error. propagate it...
let val = do_thing()?;

// ...or match every outcome, exhaustively, or it doesn't compile.
match do_thing() {
    Ok(val) => use_it(val),
    Err(e)  => recover(e),
}

exhaustive match, Result, no nil, and an ownership model that turns a whole category of runtime bug into a red squiggle before i’ve even saved the file. the compiler stops being a spellchecker and becomes a design partner that argues with me — and it’s usually right.

  • it taught me things. ownership, lifetimes, where allocations actually happen: a decade of garbage-collected languages had let me never think about any of it. learning rust properly made me a measurably better go programmer, which i did not see coming.
  • fit. clis, wasm, library crates other programs link against — these are exactly the artifacts where a runtime and a gc are dead weight. rust is just built for them.

the line, said plainly

so here’s the actual decision rule, stripped of ideology:

  • is the artifact a service or a tool? a long-running thing serving requests, or a program that does one job and a library others build on? services go to go. tools go to rust.
  • are the requirements going to thrash — product still finding its shape — or are they stable, a format or a protocol that’s been decided? thrash → go, because i need to move fast and cheaply. stable → rust, because i can afford to encode the invariants and then cash in the guarantees for years.
  • will i need to onboard someone, or read it half-asleep during an incident? → go, every time.

that’s the whole framework. notice “which language is better” appears nowhere in it. the question was never which is better. it’s what is this, and what do i actually need from it.

the honest part: what the second language costs me

i’m not going to sell you rust as free, because it isn’t, and the two things it charges me are precisely the two things go is best at.

the borrow checker genuinely slows me down. i have lost whole afternoons rearranging code to convince the compiler of something i already knew was true — the wasm chart post has a static mut wart that’s a monument to exactly this kind of fight. sometimes that friction catches a real bug and i thank it. sometimes it’s pure tax on a thing that was already correct, and in go i’d have written it and moved on ten minutes ago.

i lose go’s simplicity, and simplicity is not a small thing. go has one obvious way to do most things. rust has five, three of them involve traits, and choosing between them is a decision go never made me make. that cognitive overhead is real and constant, and it’s the whole reason i would never — never — write a plain crud api in rust. i’d be paying for guarantees the problem doesn’t need and resenting every semicolon. wrong tool. the resentment is information.

so no, i didn’t “graduate” from go to rust. that framing is the exact tribal nonsense i’m arguing against — as if languages were rungs on a ladder and the goal was to climb. i kept go for the jobs go wins and added rust for the jobs it wins. running two languages has a real cost — two toolchains, two sets of idioms rattling around my head — but for me it clears the bar easily, because the alternative is forcing one tool to do a job it’s bad at. i’ve done plenty of that. i know exactly how it ends.

lessons

  • stop asking “go or rust.” ask “service or tool, thrash or stable, move fast or be right.” the language falls out of the answer, and the fight evaporates.
  • the best reason to learn a second, stricter language isn’t to replace the first. it’s that the stricter one teaches you things that make you better in the one you already had. rust made me a better go dev. genuinely did not expect that.
  • use the boring language for the thing with users. clever is a liability when the requirements won’t sit still, and in a product they never do.

if you want both halves of this in practice: the go side is the architecture and the bugs it doesn’t save you from ; the rust side is logmole and the mcp servers . same person, same year, two languages, zero contradiction. that’s not indecision. that’s just having more than one tool in the bag.

letters to the editor

no letters yet. the editor is patient.