Skip to content

Rust vs Go in 2026: How I Actually Choose

Rust vs Go in 2026: How I Actually Choose

Last month I picked Rust for an internal CLI tool that read a config file, hit two APIs, and printed a table. I knew it was the wrong call within a day. Not because Rust is bad. Because the job didn’t need anything Rust is good at, and it definitely needed the thing Rust charges you for: time. The same tool in Go would have been done before lunch.

I’ve shipped real things in both languages now. A couple of Go services that have been in production long enough to be boring, and enough Rust that the borrow checker and I have reached a working truce. So when someone asks me “Rust or Go?” I don’t have a clean answer. I have a set of questions I ask first. This post is those questions, plus the honest version of where each language made my life better and where it made it worse.

The benchmark wars miss the point

Search “rust vs go” and you’ll drown in benchmark posts. Someone writes a hello-world HTTP server in both, fires a load tester at it, and reports that Rust did 1.4x the requests per second. Cool. I have never once made a language decision based on that number.

Here’s why it doesn’t help. For the kind of work most of us do, the bottleneck isn’t the language. It’s the database, the network call, the JSON you’re parsing, the disk. A Go service and a Rust service sitting in front of the same Postgres box feel identical to your users. The CPU gap shows up when you’re doing actual compute: parsing huge files, crunching numbers, video work, cryptography. If that’s you, the benchmark matters. If you’re writing CRUD and glue, it’s noise.

The real question isn’t “which is faster.” It’s “which language’s friction can you live with.” Every language taxes you somewhere. Go taxes you at runtime, with a class of bug it lets through. Rust taxes you at compile time, with a class of bug it refuses to let through. That trade is the whole decision. Speed is a footnote.

Where Go gets out of your way

Go’s best feature is that it’s boring, and I mean that as the highest compliment I can give a language. You can hold the whole thing in your head. There’s one obvious way to do most things. New people on the team are productive in a week, not a quarter.

Concurrency is where this really shows. Goroutines are about as low-ceremony as concurrency gets:

func fetchAll(urls []string) []Page {
    results := make(chan Page, len(urls))
    for _, url := range urls {
        go func(u string) {
            results <- fetch(u)
        }(url)
    }

    pages := make([]Page, 0, len(urls))
    for range urls {
        pages = append(pages, <-results)
    }
    return pages
}

That’s it. Start a goroutine with go, pass values over a channel, collect them. No runtime to import, no executor to pick, no functions colored async or sync. The Go team’s own advice in Effective Go is to share memory by communicating over channels, and once it clicks, concurrent Go feels lighter than concurrent anything else I’ve touched.

The cost shows up later. That fetch call can panic, hand back a nil you didn’t check, or quietly return a zero value. Go’s error handling is explicit but easy to skip, and the compiler won’t stop you from skipping it. I wrote a whole post on the error handling patterns I actually use in Go because the defaults will happily let you ship a bug that Rust would have caught at your desk.

Where Rust pays you back later

Rust’s pitch is simple once you’ve felt it: a category of bug just stops happening. Null dereferences, data races, use-after-free, the collection you mutated while looping over it. The compiler rejects all of it before the code runs.

The mechanism is the type system doing real work. Instead of a value that might be null, you get an Option, and you can’t read it without handling the empty case:

fn first_admin(users: &[User]) -> Option<&User> {
    users.iter().find(|u| u.is_admin)
}

// the compiler forces the question: what if there isn't one?
match first_admin(&users) {
    Some(user) => println!("admin: {}", user.name),
    None       => println!("no admin found"),
}

There’s no path where you forget the None case, because the code won’t compile. The same idea covers concurrency. Rust’s ownership rules mean a data race isn’t a runtime gamble, it’s a compile error. The Rust book calls this fearless concurrency, and the name isn’t marketing. I’ve refactored threaded Rust code aggressively and trusted it, because if I’d broken the sharing rules the build would have told me on the spot.

This buys you something real. On a payments-adjacent service, “this class of bug cannot reach production” is not a luxury. It’s the reason you’d pick Rust even though it’s slower to write. You pay upfront so you don’t pay at 3am.

Concurrency: goroutines vs async Rust

This is the comparison that actually decides things for me, so it gets its own section.

Go’s model is one model. You write normal-looking code, you put go in front of it, the runtime schedules it. Blocking and non-blocking code look the same. There’s nothing to choose.

Async Rust asks more of you. You pick a runtime, usually Tokio, and your functions split into two colors, async and not, which don’t mix freely:

async fn fetch_all(urls: Vec<String>) -> Vec<Page> {
    let handles: Vec<_> = urls
        .into_iter()
        .map(|u| tokio::spawn(async move { fetch(u).await }))
        .collect();

    let mut pages = Vec::new();
    for h in handles {
        pages.push(h.await.unwrap());
    }
    pages
}

It works, it’s fast, and the borrow checker still has your back across .await points. But there’s more to learn first: what a runtime is, why functions are colored, what Send means for a task you spawn. Go hands you concurrency on day one. Rust hands you a more powerful version on roughly day thirty.

So the question I ask is: how much concurrent code will this project really have, and who maintains it? A team of three rotating through a service wants Go’s one model. A small group doing heavy concurrent work that has to be correct will get more out of Rust, once they’ve paid the learning cost.

Compile times, tooling, and the parts nobody markets

The marketing pages won’t tell you this, so I will.

Go compiles fast enough that the build feels instant. Edit, run, see what happened. The toolchain is one binary that handles building, testing, formatting, and dependencies, with nothing to argue about. That tight loop matters more for daily happiness than almost any language feature.

Rust does not compile fast. A real project’s clean build is a coffee break, and incremental builds still make you wait. cargo is excellent, easily my favorite package manager, but no amount of tooling polish buys back the compile time. The borrow checker, even after it stops being your enemy, is a tax on every change. Sometimes you know your code is fine and you still have to prove it.

Both languages are pleasant once you’re fluent. They just front-load the pain differently. Go’s bad day is a nil pointer in production at 2am. Rust’s bad day is forty minutes arguing with the compiler about a lifetime. I bet real money on Rust for one service because I wanted the first kind of bad day to be impossible, and I wrote up how I structured its error handling when that call paid off.

Rust has also been the most-admired language in Stack Overflow’s developer survey for several years running. That tells you people who use it tend to want to keep using it. It does not tell you it’s the right pick for your CRUD app.

How I actually choose

Here’s the actual flowchart in my head, no benchmarks involved.

Default to Go. For services, CLIs, glue code, anything a team rotates through, anything that has to ship this quarter, Go’s speed of development and low cognitive cost win. Most software is this.

Reach for Rust when the cost of a bug is high, or when you’re doing real compute. Payments, infrastructure other teams depend on, a parser, a data pipeline doing heavy lifting, anything embedded. There, “the compiler refuses to let this bug exist” is worth the slower build and the steeper ramp.

And be honest about the team. Rust with one person who knows it and three who don’t is a bus-factor problem wearing a performance costume. Go is easier to staff, and that is a real engineering input, not a soft one.

If you want to test this for yourself without betting a project on it: take one small service you’d normally write in Go and write it in Rust this week. A webhook receiver, a little CLI, something with a clear edge. You’ll feel both the tax and the payback inside a few hours, and that feeling will tell you more than any benchmark. I keep notes on these experiments and the trade-offs in my work on backend systems, because the right answer keeps depending on the project, and that’s fine.