Migrating from Go to Rust

An honest, backend-focused comparison of Go and Rust, exploring tooling, type systems, and why teams choose to migrate.
Out of all the migrations I help teams with, Go to Rust is a bit of an outlier. It’s not a question of “is Rust faster?” or “does Rust have types?”, Go already gets you most of the way there. The discussion is mostly about correctness guarantees, runtime tradeoffs, and developer ergonomics.
A quick disclaimer before we start: this guide is heavily backend-focused. Backend services are where Go is strongest, small static binaries, a standard library focused on networking, and an ecosystem of libraries for HTTP servers, gRPC, databases, etc.
That’s also where most teams considering Rust are coming from (at least the ones who reach out to me), so I think that’s the comparison that’s actually useful in practice. If you’re writing CLI tools, embedded firmware, or game engines, some of this still applies, but to be honest, I’m afraid this is not the best resource for you.
For context, I’ve written about Go and Rust before: “Go vs Rust? Choose Go.” back in 2017, and later the “Rust vs Go: A Hands-On Comparison” with the Shuttle team, which walks through a small backend service in both languages.
What you will learn in this article
- Where Go and Rust overlap, and where they diverge.
- How Go patterns map to Rust.
- What you gain from the borrow checker.
- Where I tell people to keep Go and where Rust is worth the migration cost.
- How to migrate Go services incrementally.
I’ll be upfront: I’m not a fan of Go. I think it’s a badly designed language, even if a very successful one. It confuses easiness with simplicity, and several of its core design tradeoffs (nil everywhere, error handling as a discipline rule rather than a type, the long absence of generics) point in a direction I disagree with.
That said, success matters! Go has captured a real and persistent share of working developers, hovering around 17–19% in the JetBrains Developer Ecosystem Survey. Rust is growing steadily but is still a smaller slice:
Go is clearly working for a lot of people, and a guide that pretends otherwise isn’t helpful. So I’ll do my very best to be objective in this guide rather than relitigate old arguments. But you should know my priors so you can calibrate.
The other prior worth disclosing: I run a Rust consultancy; of course I’m biased! More people using Rust is good for my business. But I’ve also worked in both languages professionally and shipped Go services to production.
This guide is for Go developers who want an honest, side-by-side look at what changes when you move to Rust.
For a deliberately opposite take, I recommend reading “Just Fucking Use Go” by Blain Smith. Holding both views in your head at once is more useful than either one alone.
If you prefer to watch rather than read, here’s a video from the Shuttle article above, read and commented by the Primeagen:
Go developers already have one of the cleanest toolchains in the industry. Back in the day, it started off a trend of “batteries included” toolchains that give you a single, consistent interface for building, testing, formatting, linting, and managing dependencies. I’m glad that Rust followed suit, because it’s a great model. It’s one of my favorite parts about both ecosystems.
cargo has even more built-in:
| Go tool | Rust equivalent | Notes |
|---|---|---|
| go.mod / go.sum | Cargo.toml / Cargo.lock | Project config and dependency manifest |
| go get / go mod tidy | cargo add / cargo update | Add and resolve dependencies |
| go build | cargo build | Compile the project |
| go run . | cargo run | Build and run |
| go test ./... | cargo test | Testing built into the toolchain |
| go vet ./... | cargo clippy | Linter, Clippy is significantly more opinionated than vet |
| gofmt / goimports | cargo fmt | Auto-formatter, zero config |
| golangci-lint run | cargo clippy -- -D warnings | Strict lint mode |
| go install ./cmd/foo | cargo install --path . | Install a binary |
| go doc | cargo doc --open | Generate and view API docs |
| pprof | cargo flamegraph / samply | CPU profiling |
| govulncheck | cargo audit | Vulnerability scanning against an advisory database |
The big difference is that in Go you typically reach for third-party tools (golangci-lint, mockgen, air, goreleaser) to fill gaps.
In Rust, the first-party ecosystem covers more out of the box.
Things that do require external crates (e.g. cargo watch, cargo nextest) install with one command and feel native, e.g. cargo install cargo-nextest gives you cargo nextest right away.
Both communities have converged on the same insight about formatters: a single canonical style, even an imperfect one, is worth more than the bikeshedding it eliminates.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite. — Rob Pike, Go Proverbs
The same is true of rustfmt: not everyone likes every detail, but the absence of style debates in code review is worth far more than the occasional formatting preference you’d have made differently.
| Go | Rust | |
|---|---|---|
| Stable Release | 2012 | 2015 |
| Type System | Static, structural, generics since 1.18 | Static, nominal, generics + traits + lifetimes |
| Memory Management | Garbage collected (concurrent, low-pause) | Ownership and borrowing, no GC |
| Null Safety | nil is everywhere | No null; Option<T> is the type-level replacement |
| Error Handling | error interface, if err != nil { ... } | Result<T, E> , ? operator, exhaustive matching |
| Concurrency | Goroutines + channels (CSP) | async /await on tokio + channels + threads |
| Cancellation | context.Context (convention, not enforced) | CancellationToken / explicit, type-checked plumbing |
| Data Races | Caught at runtime via -race (probabilistic, at runtime) | Caught at compile time by Send /Sync |
| Compile Times | Very fast | Slow, especially clean builds |
| Runtime | ~2 MB Go runtime + GC | None beyond libc (or fully static with MUSL) |
| Binary Size | Small to medium (a few MB) | Comparable; very small with panic = "abort" + LTO |
| Learning Curve | Gentle | Steep |
| Ecosystem Size | ~750k+ modules | 250,000+ crates |
The headline is that Go and Rust are both compiled, statically typed, single-binary-deploy languages with strong concurrency stories. The differences are about what guarantees you get from the compiler and how much control you have over runtime behaviour.
One framing that helps before we go further: most of what changes when you move from Go to Rust is that checks get pulled into the type system. Nil-handling, error propagation, data races, resource lifetimes, cancellation, generics, these are all things Go relies on convention, tooling (go vet, errcheck, golangci-lint, -race), or runtime detection to keep honest. Rust encodes them as types the compiler enforces directly.
The common pushback is that this means “more cognitive overhead.” I’d challenge that. It’s more upfront, yes, but it’s also harder to hold wrong. A Mutex<T> in Rust doesn’t just document that the data needs a lock, it makes the lock the only way to reach the data: you call .lock(), you get a guard, and the guard is what gives you access to the inner value. Drop the guard and the lock releases automatically. There is no “I forgot to lock” path because the unlocked path doesn’t exist in the type. Once you internalize that pattern, and you find it repeated everywhere (Option, Result, &mut T, Send/Sync, RAII guards), Rust stops feeling heavy and starts feeling like the compiler is doing work you used to do in your head.
Go developers don’t usually come to Rust because Go is “too slow.”
For most backend workloads, Go is plenty fast.
People are generally a bit frustrated with Go’s verbose error handling, the danger of segmentation faults from nil pointers, and the lack of generics (for a long time) or any sophisticated type system features, such as
Source: Hacker News

















