Michiel Mulders Michiel loves the Node.js and Go programming languages. A backend/core blockchain developer and avid writer, he's very passionate about blockchain technology.

When to use Rust and when to use Go

7 min read 2000

Editor’s note: This article was updated on 1 June 2022 to reflect more up-to-date information about Rust and Go.

Right off the bat, there are clear differences between Go and Rust. Go has a stronger focus on building web APIs and small services that can scale endlessly, especially with the power of Goroutines. The latter is also possible with Rust, but things are much harder from a developer experience point of view.

Rust works well for processing large amounts of data and other CPU-intensive operations, such as executing algorithms. This is Rust’s biggest edge over Go; projects that demand high performance are generally better suited for Rust.

In this tutorial, we’ll compare and contrast Go and Rust, evaluating each programming language for performance, concurrency, memory management, and the overall developer experience. We’ll also present an overview of these elements to help you pick the right language for your project at a glance.

If you’re just starting out with Rust, it might be a good idea to brush up on this beginner’s guide before you read any further.

If you’re all caught up, let’s dive in!

Contents

Performance

Originally designed by Google’s engineers, Go was introduced to the public in 2009. It was created to offer an alternative to C++ that was easier to learn and code and was optimized to run on multicore CPUs.

Since then, Go has been great for developers who want to take advantage of the concurrency the language offers. The language provides Goroutines that enable you to run functions as subprocesses.

A big advantage of Go is how easily you can use Goroutines. Simply adding the go syntax to a function makes it run as a subprocess. Go’s concurrency model allows you to deploy workloads across multiple CPU cores, making it a very efficient language:

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")

    go f("goroutine")
    time.Sleep(time.Second)
    fmt.Println("done")
}

Rust was designed from the beginning to be high-performance – in fact, it’s the first answer to “Why Rust?” on the Rust website! The way Rust handles memory management means that it doesn’t need a garbage collector, unlike Go, and the use of references let objects easily get passed around without requiring copies to be made.

Rust vs. Go benchmarks

Individual benchmarks can be game-able and also tricky to interpret. The Benchmarks Game deals with this by allowing multiple programs for each language, and compares how long they take to run as well as memory usage and code complexity to get a better sense for what the tradeoffs are between languages.

For all of the tested algorithms, the most optimized Rust code was at least 30 percent faster than the most optimized Go code, and in many cases it was significantly more; for the binary-trees benchmark, the most optimized Rust code was 12 times faster than the most optimized Go code! In many cases, even the least optimized Rust code was faster than the most optimized Go code.

Here are a few examples for the most optimized Rust and Go code:

Benchmark game binary trees

benchmark game pidigits

benchmark game madelbot

Scalability

Both languages are good at scaling up to take advantage of many CPUs to process data in parallel. In Go, you can use a Goroutine to process each piece of data and use a WaitGroup to wait for them all to finish. In Rust, rayon is a very handy crate that makes it easy to iterate over a container in parallel.

Concurrency

As mentioned above, Go supports concurrency. For example, let’s say you’re running a web server that handles API requests. You can use Go’s Goroutines to run each request as a subprocess, maximizing efficiency by offloading tasks to all available CPU cores.

Goroutines are part of Go’s built-in functions, while Rust has only received native async/await syntax to support concurrency. As such, the developer experience edge goes to Go when it comes to concurrency. However, Rust is much better at guaranteeing memory safety.

Here’s an example of simplified threads for Rust:

use std::thread;
use std::time::Duration;

fn main() {
   // 1. create a new thread
   for i in 1..10 {
      thread::spawn(|| {
         println!("thread: number {}!", i);
         thread::sleep(Duration::from_millis(100));
      });
   }

  println!("hi from the main thread!");
}

Concurrency has always been a thorny problem for developers. It’s not an easy task to guarantee memory-safe concurrency without compromising the developer experience. However, this extreme security focus led to the creation of provably correct concurrency.

Rust experimented with the concept of ownership to prevent unsolicited access of resources to prevent memory safety bugs. Rust offers four different concurrency paradigms to help you avoid common memory safety pitfalls. We’ll take a closer look at two common paradigms in the following sections: channel and lock.

Channel

A channel helps transfer a message from one thread to another. While this concept also exists for Go, Rust allows you to transfer a pointer from one thread to another to avoid racing conditions for resources. Through passing pointers, Rust can enforce thread isolation for channels. Again, Rust displays its obsession with memory safety in regards to its concurrency model.

Lock

Data is only accessible when the lock is held. Rust relies on the principle of locking data instead of cod, which is often found in programming languages such as Java.

For more details on the concept of ownership and all concurrency paradigms, check out “Fearless Concurrency with Rust.”

Memory safety

The earlier concept of ownership is one of Rust’s main selling points. Rust takes type safety, which is also important for enabling memory-safe concurrency, to the next level.

According to the Bitbucket blog, “Rust’s very strict and pedantic compiler checks every variable you use and every memory address you reference. It avoids possible data race conditions and informs you about undefined behavior.”

This means you won’t end up with a buffer overflow or a race condition due to Rust’s extreme obsession with memory safety. However, this also has its disadvantages. For example, you have to be hyperaware of memory allocation principles while writing code. It’s not easy to always have your memory safety guard up.



Developer experience

First of all, let’s look at the learning curve associated with each language. Go was designed with simplicity in mind. Developers often refer to it as a “boring” language, which is to say that its limited set of built-in features makes Go easy to adopt.

Furthermore, Go offers an easier alternative to C++, hiding aspects such as memory safety and memory allocation. Rust takes another approach, forcing you to think about concepts such as memory safety. The concept of ownership and the ability to pass pointers makes Rust a less attractive option to learn. When you’re constantly thinking about memory safety, you’re less productive and your code is bound to be more complex.

The learning curve for Rust is pretty steep compared to Go. It’s worth mentioning, however, that Go has a steeper learning curve than more dynamic languages such as Python and JavaScript.

Dev cycles

For modern software developers, being able to iterate quickly is very important, and so is being able to have multiple people working on the same project. Go and Rust achieve these goals in somewhat different ways.

The Go language is very simple to write and understand, which makes it easy for developers to understand each other’s code and extend it. However, in Go code you have to be very careful about error-checking and avoiding nil accesses; the compiler doesn’t provide a lot of help here, and so implicitly understanding which variables might be nil and which ones are guaranteed to be non-nil.

Rust code is trickier to write and make compile; developers have to have a good understanding about references and lifetimes and such to be successful. However, the Rust compiler does an excellent job of catching these issues. (and emitting incredibly helpful error messages – in a recent survey 90 percent of Rust developers approved of them!) So while “Once your code compiles, it’s correct!” isn’t true for either language, it’s closer to true for Rust, and this gives other developers more confidence in iterating on existing code.

Features

Both languages have a solid assortment of features. As we’ve seen above, Go has built-in support for a number of useful concurrency mechanisms, chiefly Goroutines and channels. The language supports interfaces and, as of Go v1.18 which was released in March 2022, generics. However, Go does not support inheritance, method or operator overloading, or assertions. Because Go was developed at Google, it’s no surprise that Go has very good support for HTTP and other web APIs, and there’s also a large ecosystem of Go packages.

The Rust language is a bit more feature full than Go’s; it supports traits (a more sophisticated version of interfaces), generics, macros, and rich built-in types for nullable types and errors, as well as the ? operator for easy error handling. It’s also easier to call C/C++ code from Rust than it is from Go. Rust also has a large ecosystem of crates.

When to use Go

Go works well for a wide variety of use cases, making it a great alternative to Node.js for creating web APIs. As noted by Loris Cro, “Go’s concurrency model is a good fit for server-side applications that must handle multiple independent requests”. This is exactly why Go provides Goroutines.

What’s more, Go has built-in support for the HTTP web protocol. You can quickly design a small API using the built-in HTTP support and run it as a microservice. Therefore, Go fits well with the microservices architecture and serves the needs of API developers.

In short, Go is a good fit if you value development speed and prefer syntax simplicity over performance. On top of that, Go offers better code readability, which is an important criterion for large development teams.

Choose Go when:

  • You care about simplicity and readability
  • You want an easy syntax to quickly write code
  • You want to use a more flexible language that supports web development

When to use Rust

Rust is a great choice when performance matters, such as when you’re processing large amounts of data. Furthermore, Rust gives you fine-grained control over how threads behave and how resources are shared between threads.

On the other hand, Rust comes with a steep learning curve and slows down development speed due to the extra complexity of memory safety. This is not necessarily a disadvantage; Rust also guarantees that you won’t encounter memory safety bugs as the compiler checks each and every data pointer. For complex systems, this assurance can come in handy.

Choose Rust when:

  • You care about performance
  • You want fine-grained control over threads
  • You value memory safety over simplicity

Go vs. Rust: My honest take

Let’s start by highlighting the similarities. Both Go and Rust are open-source and designed to support the microservices architecture and parallel computing environments. Both optimize the utilization of available CPU cores through concurrency.

But at the end of the day, which language is best?

There many ways to approach this question. I’d recommend thinking about what type of application you want to build. Go serves well for creating web applications and APIs that take advantage of its built-in concurrency features while supporting the microservices architecture.

You can also use Rust to develop a web API, but it wasn’t designed with this use case in mind. Rust’s focus on memory-safety increases complexity and development time, especially for a fairly simple web API. However, the larger amount of control you have over your code allows you to write more optimized, memory-efficient, and performant code.

To put it as simply as possible, the Go versus Rust debate is really a question of simplicity versus security.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Michiel Mulders Michiel loves the Node.js and Go programming languages. A backend/core blockchain developer and avid writer, he's very passionate about blockchain technology.

12 Replies to “When to use Rust and when to use Go”

  1. Your Rust thread example doesn’t spin up 10 threads, which I think was intended. It just runs a for loop in a single thread

  2. Uh… What?

    “Again, Rust betrays its obsession with memory safety in regards to its concurrency model.”

  3. You didn’t compare Go very well in the last half of the post, in my opinion. You focused very heavily on how Rust approaches those topics instead of mentioning how Go handles them.

    A pet peeve of mine is also “Golang” is not the name of the language as they state on the site due to the confusion of registering Golang[.]org.

  4. What do you mean by the workd “subprocess” when talking about Go? Do you mean to say that the application will literally fork(…) off a child process? I’ll admit that I’ve never used Go, but that was not my impression of its architecture–I didn’t think that there was even a one-to-one correspondence of invoked goroutines to threads, but instead that it had an M-to-N threading model based on Go’s own implementation of pre-emption inside of its runtime. I would appreciate clarification.

  5. Hi Matt, thanks for your comment and honesty!

    The goal of this post is to provide a high-level overview between both languages for developers who need to decide between both languages. I’ve included some code snippets but don’t want to go into detail for each element. A great resource for looking up all discussed elements in this post is https://gobyexample.com/ 🙌

    Besides that, we’ve updated the post’s title from “Golang” to “Go”. Thanks for notifying us about this! 🙂

  6. Hi Ian, no worries, let me first clarify Goroutines using Go’s documentation (http://golang.org/doc/effective_go.html#goroutines).

    > They’re called goroutines because the existing terms—threads, coroutines, processes, and so on—convey inaccurate connotations. A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.

    > Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

    In short, Go multiplexes multiple goroutines into threads. So you are right about to M-to-N threading model. I’ve used the word “subprocess” here losely to make the concept more clear. It’s not 100% accurate. If this is confusing, feel free to ping me @michielmulders -> We want to avoid confusion! 🙂

  7. I think your language got confused between concurrency and parallelism while talking about Go. Go concurrency doesn’t spawn tasks across multiple CPUs, it maximises the single CPU utilisation by running sub processes concurrently (not in parallel)

  8. Go harder to learn than JS? You have to RE LEARN working with JS every time a new framework is launched!

  9. I get an error on line 7:
    thread::spawn(|| {

    error[E0373]: closure may outlive the current function, but it borrows `i`, which is owned by the current function

    Fortunately the Rust compiler tells me how to fix it:

    help: to force the closure to take ownership of `i` (and any other referenced variables), use the `move` keyword
    thread::spawn(move || {

Leave a Reply