Go or Rust: a detailed comparison and which language to choose
How Go and Rust differ in practice: fourteen criteria, six differences you feel in work, which language for which task, the same tasks written in both and common mistakes.
In short
Go and Rust are both compiled, fast and memory-safe, but they are built for different things. Go bets on simplicity: a small language, a garbage collector, goroutines, compilation in seconds and a team that becomes productive in weeks — the default for web services, APIs and cloud tools. Rust bets on control: no garbage collector, ownership checked by the compiler, no data races in safe code and predictable latency — the choice for systems software, high-performance engines, WebAssembly and places where every millisecond and megabyte counts. For a typical backend, Go gets you to production faster; Rust pays off where Go’s pauses or memory use become the problem.
In short: which one to choose
If you are building an API, a web service, a bot backend, a queue worker or a cloud tool — take Go. The language is learned quickly, code reads the same in every team, compilation takes seconds, and the garbage collector’s pauses are short enough for almost any web load.
If you are building a database engine, a proxy that must never pause, a library for WebAssembly, firmware, or code that will run billions of times — take Rust. You pay with a steep learning curve and slow compilation and get maximum speed, minimal memory and errors caught before launch.
- Web services and APIs — Go
- Systems and every millisecond — Rust
- Both are safe and compiled
Go vs Rust: a detailed comparison
Fourteen criteria side by side. Speed depends on the code, so the table shows relative positions rather than benchmarks.
| Criterion | Go | Rust |
|---|---|---|
| Origin | Google, 2009; version 1.0 in 2012 | Mozilla, 2010; version 1.0 in 2015; now the Rust Foundation |
| Philosophy | simplicity and one obvious way | control and zero-cost abstractions |
| Memory | a garbage collector with short pauses | ownership, no collector |
| Data races | possible; a race detector finds them in tests | impossible in safe code |
| Concurrency | goroutines and channels, built in | threads and async with Tokio |
| Errors | values and if err != nil | Result and the ? operator |
| Speed | high | the highest, close to C++ |
| Latency | stable, with rare short pauses | fully predictable |
| Memory use | moderate | minimal |
| Compilation | seconds | noticeably slower |
| Entry bar | low: weeks | high: months |
| Ecosystem | cloud and web: Docker, Kubernetes, Terraform | systems, WebAssembly, tooling: ripgrep, Tauri, Rolldown |
| Hiring | easier | harder, fewer specialists |
| Best for | APIs, web services, workers, cloud tools | engines, proxies, WebAssembly, embedded |
6 differences you feel in everyday work
-
The first month
In Go a new developer ships features in the first weeks; in Rust the first weeks go into arguing with the borrow checker.
-
Speed of iterations
Go compiles a service in seconds; a large Rust project can take minutes after a change.
-
Where errors are caught
Rust catches more at compile time — races, null values, unhandled errors; Go leaves part of it to tests.
-
Reading someone else’s code
Go code looks the same everywhere; Rust code varies with the author’s use of traits and macros.
-
Under extreme load
When the garbage collector’s pauses start to show in the slowest requests, Rust removes them completely.
-
Deployment
Both produce one binary without dependencies; Go cross-compiles for any platform with one variable.
Which language for which task
Twelve typical projects with a recommendation and the reason.
| Task | Take | Why |
|---|---|---|
| REST API or web service | Go | fast development, a strong standard library |
| Bot backend or queue worker | Go | goroutines and simple code |
| Microservices in a team | Go | easy to hire and to read each other’s code |
| Cloud and DevOps tools | Go | the home of Docker and Kubernetes |
| Command-line utility | either | Go is faster to write, Rust is faster to run |
| High-load proxy or gateway | Rust | no pauses and minimal memory |
| Database or search engine | Rust | control over every byte |
| WebAssembly in the browser | Rust | small and fast modules without a runtime |
| Embedded devices | Rust | works without an operating system |
| Desktop app with a web interface | Rust | Tauri: a light alternative to Electron |
| MVP of a startup | Go | speed of development matters more than the last percent of speed |
| Code with strict reliability requirements | Rust | the compiler proves more |
The same tasks in Go and Rust: 6 examples
Parallel work, errors and polymorphism written in both languages. Go 1.26 is checked by gofmt and go vet, Rust 1.97 is compiled with warnings as errors; the output is in the comments.
Parallel work: Go
A goroutine per text and a WaitGroup that waits for all of them.
package main
import (
"fmt"
"strings"
"sync"
)
// count words in several texts at once: a goroutine per text
func main() {
texts := []string{"oak table and oak chair", "pine shelf", "walnut desk for the office"}
counts := make([]int, len(texts))
var wg sync.WaitGroup
for i, t := range texts {
wg.Go(func() { // Go 1.25+: WaitGroup.Go starts a goroutine and counts it
counts[i] = len(strings.Fields(t))
})
}
wg.Wait()
fmt.Println(counts) // [5 2 5]
}
Parallel work: Rust
Scoped threads: each thread gets its own cell, and the compiler proves there is no race.
use std::thread;
// the same in Rust: scoped threads; the compiler proves there are no data races
fn main() {
let texts = ["oak table and oak chair", "pine shelf", "walnut desk for the office"];
let mut counts = [0; 3];
thread::scope(|s| {
for (count, text) in counts.iter_mut().zip(texts) {
s.spawn(move || *count = text.split_whitespace().count());
}
});
println!("{counts:?}"); // [5, 2, 5]
}
Errors: Go
An error is a value: it is wrapped with context and checked with errors.Is.
package main
import (
"errors"
"fmt"
"strconv"
)
var ErrTooBig = errors.New("too many items")
// an error is an ordinary value: it is returned and checked explicitly
func parseQty(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("quantity %q: %w", s, err)
}
if n > 100 {
return 0, ErrTooBig
}
return n, nil
}
func main() {
for _, in := range []string{"3", "abc", "500"} {
n, err := parseQty(in)
if errors.Is(err, ErrTooBig) {
fmt.Println("limit exceeded")
continue
}
fmt.Println(n, err)
}
// 3 <nil>
// 0 quantity "abc": strconv.Atoi: parsing "abc": invalid syntax
// limit exceeded
}
Errors: Rust
The error is part of the type, ? passes it up, and match must handle every variant.
use std::num::ParseIntError;
enum QtyError {
NotANumber(ParseIntError),
TooBig,
}
// Result: the error is part of the type, and the compiler will not let you ignore it
fn parse_qty(s: &str) -> Result<u32, QtyError> {
let n: u32 = s.parse().map_err(QtyError::NotANumber)?;
if n > 100 {
return Err(QtyError::TooBig);
}
Ok(n)
}
fn main() {
for input in ["3", "abc", "500"] {
match parse_qty(input) {
Ok(n) => println!("{n}"),
Err(QtyError::TooBig) => println!("limit exceeded"),
Err(QtyError::NotANumber(e)) => println!("not a number: {e}"),
}
}
// 3
// not a number: invalid digit found in string
// limit exceeded
}
Polymorphism: Go interfaces
A type satisfies an interface just by having the method — no declarations.
package main
import "fmt"
// an interface is satisfied implicitly: it is enough to have the right method
type Shipping interface {
Price(kg float64) float64
}
type Courier struct{ Base float64 }
type Pickup struct{}
func (c Courier) Price(kg float64) float64 { return c.Base + 50*kg }
func (Pickup) Price(float64) float64 { return 0 }
func main() {
options := []Shipping{Courier{Base: 300}, Pickup{}}
for _, o := range options {
fmt.Printf("%T: %.0f\n", o, o.Price(2))
}
// main.Courier: 400
// main.Pickup: 0
}
Polymorphism: Rust traits
A trait is implemented explicitly, and dynamic dispatch is visible in the type — Box<dyn Shipping>.
// a trait is implemented explicitly, and the compiler checks every implementation
trait Shipping {
fn price(&self, kg: f64) -> f64;
}
struct Courier {
base: f64,
}
struct Pickup;
impl Shipping for Courier {
fn price(&self, kg: f64) -> f64 {
self.base + 50.0 * kg
}
}
impl Shipping for Pickup {
fn price(&self, _kg: f64) -> f64 {
0.0
}
}
fn main() {
let options: Vec<(&str, Box<dyn Shipping>)> =
vec![("Courier", Box::new(Courier { base: 300.0 })), ("Pickup", Box::new(Pickup))];
for (name, o) in &options {
println!("{name}: {:.0}", o.price(2.0));
}
// Courier: 400
// Pickup: 0
}
Common mistakes when choosing
-
Rust for an ordinary API
Months of learning and slow builds for a gain the users will not notice.
-
Go where pauses are unacceptable
For hard real-time and huge heaps the collector becomes the bottleneck.
-
Choosing by benchmarks
In a web service the time is usually spent in the database and the network, not in the language.
-
Forgetting the team
A language nobody on the team can support is a risk bigger than any speed difference.
-
Rewriting everything at once
Move only the slow part to Rust; the rest can stay in Go.
Questions about Go and Rust
Which is faster, Go or Rust?
Rust is usually faster and uses less memory, but for a web service the difference is often invisible: the time goes to the database and the network.
Which is easier to learn?
Go — by far: a small language that takes weeks. Rust takes months because of ownership and lifetimes.
What should a backend be written in?
For most APIs and services — Go. Rust for the parts where every millisecond and megabyte matters.
Is Rust safer than Go?
Both are memory-safe. Rust additionally rules out data races at compile time; in Go they are found by the race detector in tests.
Can Go and Rust be used together?
Yes, as separate services that talk over the network — the most common and simplest way.
Which pays more?
Specialists in both are well paid; there are fewer Rust developers, so they are harder to find.
Does Go have generics?
Yes, since Go 1.18 (2022). Rust has had generics and traits from the start.
Online form
Services
in Go
I write services in Go where response speed and load matter. If your task really needs Rust, I will say so honestly and help find the right solution. Tell me about the project — I answer within one working day.