Rust vs Go in 2024

Rust vs Go in 2024

rust-vs-go.png

Can I say something without everyone getting mad?

Which is better, Rust or Go? Which language should you choose for your next project, and why? How do the two compare in areas like performance, simplicity, safety, features, scale, and concurrency? What do they have in common, and where do they fundamentally differ? Let’s find out, in this friendly and even-handed comparison of Rust and Golang.

As it happens, I teach (and write about) both Go and Rust professionally, and I’m also a keen user of both languages, which I suspect makes me something of an outlier: I’ve got a foot in both camps! Since there can be a fair amount of perfectly understandable tribal feeling about one’s favourite programming language, maybe it’ll be helpful to get a new perspective, from someone who loves both Rust and Go. Here’s why.

Rust and Go are both awesome

First, it’s really important to say that both Go and Rust are absolutely excellent programming languages. They’re modern, powerful, widely-adopted, and offer excellent performance.

Rust is a low-level statically-typed multi-paradigm programming language that’s focused on safety and performance.

Gints Dreimanis

Whereas:

Go is an open-source programming language that makes it easy to build simple, reliable, and efficient software.

golang.org

In this article, I’ll try to give a brief overview of where I think Go is the ideal choice, and where I think Rust is a better alternative.

Similarities

What are some of the common goals of both languages?

Memory safety

Historically, one of the biggest causes of software bugs and security vulnerabilities has been accessing memory unsafely or incorrectly.

Rust and Go deal with this problem in different ways, but both aim to be smarter and safer than other languages about managing memory.

The genius of Go is that it has a garbage collector. The genius of Rust is that it doesn’t need one.

Fast, compact executables

They’re both compiled languages, which means your programs are translated directly to executable machine code, so that you can deploy your program as a single binary file. This also makes both Rust and Go programs extremely fast in comparison to interpreted languages such as Python or Ruby.

General-purpose languages

Rust and Go are also both powerful, scalable general-purpose programming languages, which you can use to develop all kinds of modern software. Both have excellent standard libraries and a thriving third-party ecosystem, as well as great commercial support and a large user base.

Pragmatic programming style

While both Go and Rust have features associated with functional and object-oriented programming (OOP), they’re pragmatic languages aimed at solving problems in whatever way is most appropriate.

Development at scale

Both Rust and Go have some useful features which make them suitable for programming in the large, whether that means large teams, or large codebases, or both.

For example, both Rust and Go use a standard code formatting tool (gofmt for Go, rustfmt for Rust), putting an end to useless arguments over where to put your brackets.

Both also have excellent, built-in, high-performance standard build and dependency management tools; no more wrestling with complex third-party build systems and having to learn a new one every couple of years.

Differences

While Rust and Go have a lot in common, there are also a few areas where a reasonable person might prefer one language over the other, to meet the specific needs of their project.

Performance

Both Go and Rust are very fast. However, while Go’s design favours fast compilation, Rust is optimised for fast execution.

Rust’s run-time performance is also more consistent, because it doesn’t use garbage collection. On the other hand, Go’s garbage collector takes some of the burden off the programmer, making it easier to focus on solving the main problem, rather than the fine detail of memory management.

Rust is a better choice for areas where speed of execution beats all other considerations, such as game programming, operating system kernels, web browser components, and real-time control systems.

Simplicity

Go is incredibly easy to learn. I know this is an often-touted benefit, but I was really surprised at how quickly I was able to be productive. Thanks to the language, docs, and tools, I was writing interesting, committable code after literally two days.
Early Impressions of Go From a Rust Programmer

Go is a small language, by design: it has very little syntax, few keywords, and as few language constructs as it can get away with. You can learn the basics of Go and be productive in the language very quickly.

That gives Go the advantage in projects with a short timescale, or for teams that need to onboard lots of new programmers quickly, especially if they’re relatively inexperienced.

With Go, you get things done—fast. Go is one of the most productive languages I’ve ever worked with. The mantra is: solve real problems today.
Matthias Endler

On the other hand, the very simplicity of Go means that you need to write more code to solve certain problems: the language does less of the heavy lifting for you. It also doesn’t suit some programmers who like a very rich, expressive language that gives them many different ways to solve a problem.

Features

At the other end of the scale, Rust has just about every feature you could imagine in a programming language, and some you probably can’t. That makes it a powerful and expressive language, with lots of different ways to do the same thing.

Rust supports more complexity than several other programming languages, therefore, you can achieve more with it.
Devathon

If you’re transitioning to Rust from some other language, you can probably find Rust equivalents for most of the features you’re used to. That gives Rust the advantage when large projects need to be migrated from a traditional language such as C++ or Java.

Rust competes for mindshare with C++ and D for programmers who are prepared to accept more complex syntax and semantics (and presumably higher readability costs) in return for the maximum possible performance.
Dave Cheney

And, while it’s a bigger language than Go, Rust is still relatively easy to learn:

Rust is a cleaner, more easily understood and friendlier language than most—and certainly much easier to understand than modern C++.
Marc Schoolderman

On the other hand, the complexity of Rust, and the new way of thinking about problems that it requires, makes experienced Rust developers expensive, and it can take longer to ramp up junior developers to a level where they can be productive. Rust might not be the best choice for projects that need very rapid development or prototyping, or where cost is a bigger factor than safety and reliability.

To summarise:

Go is too simple to write complicated programs, while Rust is too complicated to write simple programs. It all depends which problem you’d rather have.

Concurrency

Most languages have some form of support for concurrent programming (doing multiple things at once), but Go’s support is second to none. Instead of using operating system threads, which are relatively clumsy and slow, Go provides a lightweight solution: goroutines. You can run millions of concurrent goroutines in a single program without creating serious performance problems, which makes Go the perfect choice for high-scale concurrent applications such as webservers and microservices.

Go also features fast, safe, efficient ways for goroutines to communicate and share data, using channels. Go’s concurrency support feels well-designed, and a pleasure to use. Because it was built in to the language from the start, instead of being an afterthought, concurrent programming in Go is simple and well-integrated.

Having lightweight syntax for spawning Go routines and using channels is really nice. It really shows the power of syntax that such small details make concurrent programming feel so much nicer than in other languages.
Early Impressions of Go From a Rust Programmer

This makes Go the perfect choice for high-scale concurrent applications such as webservers and microservices.

Go makes it very easy to build a nicely factored application that takes full advantage of concurrency while being deployed as a set of microservices. Rust can do those things, too, but it’s arguably a bit tougher. In some respects, Rust’s obsession with preventing memory-related security vulnerabilities means that programmers have to go out of their way to perform tasks that would be simpler in other languages, including Go.
Sonya Koptyev

Safety

Rust is carefully designed to ensure that programmers can’t do something unsafe that they didn’t mean to do, such as overwriting a shared variable. The compiler requires you to be explicit about the way you share data between different parts of the program, and can detect many common mistakes and bugs.

Rust’s very strict and pedantic compiler checks each and every variable you use and every memory address you reference. It avoids possible data race conditions and informs you about undefined behavior. Concurrency and memory safety issues are fundamentally impossible to get in the safe subset of Rust.
Why Rust?

As a result, so-called “fighting with the borrow checker” is a common complaint among new Rust programmers. Implementing your program in safe Rust code often means fundamentally re-thinking its design, which can be frustrating, but the benefits can be worth it when reliability is your top priority.

Rust’s ownership model may force you to fundamentally re-architect your program to avoid running into these issues. And that’s a good thing, if correctness and reliability are your top priority. What’s the point of a language that doesn’t change the way you program, after all? But there are other legitimate priorities.

If you choose Rust, usually you need the guarantees that the language provides: safety against null pointers and race conditions, predictable runtime behaviour, and total control over the hardware. If you don’t require any of these features, Rust might be a poor choice for your next project. That’s because these guarantees come with a cost: ramp-up time. You’ll need to unlearn bad habits and learn new concepts. Chances are, you will fight with the borrow checker a lot when you start out.
Matthias Endler

Scale

Go was designed to make it easy to scale both your projects and your development teams. Its minimalist design leads to a certain uniformity, and the existence of a well-defined standard style means that any Go programmer can read and understand a new codebase relatively quickly.

When it comes to software development in the large, clear is better than clever. Go is a good choice for big organisations, especially with many distributed teams. Its fast build times also favour rapid testing and deployment.

Trade-offs

Rust and Go’s design teams have made some starkly different choices, so let’s look at some areas where those trade-offs make the two languages very distinct from one another.

Garbage collection

Languages (like Go) that feature garbage collection, and automatic memory management in general, make it quick and easy to develop reliable, efficient programs, and for some people that’s the most important thing.

But garbage collection, with its performance overhead and stop-the-world pauses, can make programs behave unpredictably at run-time, and some people find this inconsistency unacceptable.

Languages (such as Rust) where the programmer must take explicit responsibility for allocating and freeing every byte of memory, are better for real-time or ultra-high-performance applications.

Go is a very different language to Rust. Although both can vaguely be described as systems languages or replacements for C, they have different goals and applications, styles of language design, and priorities. Garbage collection is a really huge differentiator. Having GC in Go makes the language much simpler and smaller, and easy to reason about. Not having GC in Rust makes it really fast (especially if you need guaranteed latency, not just high throughput) and enables features and programming patterns that are not possible in Go (or at least not without sacrificing performance).
PingCAP

Error handling

In some languages, when a function encounters an error (for example, something like “file not found”), it can be handled explicitly, or it can be automatically propagated back to the function’s caller, and the caller’s caller, and so on, until it either finds someone willing to handle the problem, or the program crashes with an ugly error message.

Errors that trigger an automatic return from the function if not handled are called exceptions, and, while convenient for programmers, can lead to some confusing behaviour—especially if the exceptions are used deliberately as a control flow mechanism.

In both Rust and Go, while exceptions are available, their use is strongly discouraged. Instead, Go lets the programmer choose to either ignore a possible error, or explicitly check and return it (perhaps with some added context, via the error wrapping feature).

Rust’s error handling is even more powerful, relying on built-in Option and Result types which indicate that a return value may or may not be present, or may instead be some error. If a function returns Result, for example, its return value can’t be used directly: Rust’s strict type system forces you to specify what should happen instead if there’s an error.

However, Rust also provides a neat syntactic shorthand: the ? operator causes a function to return automatically if the Option value is not present, or the Result value contains an error. Since errors happen a lot, this gives Rust programmers a more compact way to write the necessary handling code than is available in Go.

Abstraction

The history of computer programming has been a story of increasingly sophisticated abstractions that let the programmer solve problems without worrying too much about how the underlying machine actually works.

That makes programs easier to write and perhaps more portable. But for many programs, access to the hardware, and precise control of how the program is executed, are more important.

Rust aims to let programmers get “closer to the metal”, with more control, but Go abstracts away the architectural details to let programmers get closer to the problem.

The key difference between Rust and Go: you think you can understand Go code, but you’re wrong. On the other hand, you think you CAN’T understand Rust code, and you’re right.

Speed

Rust makes a number of design trade-offs to achieve the best possible execution speed. By contrast, Go is more concerned with simplicity, and it’s willing to sacrifice some (run-time) performance for it.

Whether you favour Rust or Go on this point depends on whether you spend more time waiting for your program to build, or waiting for it to run.

Correctness

Go and Rust both aim to help you write correct programs, but in different ways: Go provides a superb built-in unit testing framework, for example, and a rich standard library, while Rust is focused on eliminating run-time bugs using its borrow checker.

It’s probably fair to say that it’s easier to write a given program in Go, but the result may be more likely to contain bugs than the Rust version. Rust imposes discipline on the programmer, but Go lets the programmer choose how disciplined they want to be about a particular project.

What now?

I hope this article has convinced you that both Rust and Go deserve your serious consideration. You should reject the false dilemma that you can only learn one or the other. In fact, the more languages you know, the more valuable you are as a software developer.

If you’re new to the tech industry, getting some skills in one or both of these two languages should be your priority. If you’re an established developer who doesn’t yet have Go or Rust on your resume, I think you’d be well advised to invest some time in acquiring them. Legacy languages such as C++, Python, Java, and friends will still be around for years to come, no doubt—that’s what “legacy” means—but they wouldn’t be the first things I’d pick up. New development in many large companies is now restricted to memory-safe languages only, which effectively means either Go or Rust, and probably the latter.

Every new language you learn gives you new ways of thinking about problems, and that can only be a good thing. The most important factor in the quality and success of any software project is not the choice of language, but the skill of the programmer. You will be most skilful when using the language that suits you best, and that you enjoy programming in the most. So, if the question is “Should I learn Rust or Go?”, the only right answer is “Yes.”

More resources

Check out my selections of the best Rust books and the best Go books, and you’ll find plenty of fantastic learning materials. You might also enjoy these articles:

And here’s a recent episode of the excellent go podcast() where I discuss the complementary nature of Rust and Go, and why all good programmers should be familiar with both languages:

If you’d like to take your journey further, you might like to consider studying either Rust or Go with me, in personal, one-to-one coaching and mentoring sessions. Contact me if you have any questions—I’ll be happy to chat with you.

Generic types in Go

Generic types in Go

Suite smells: testing legacy code

Suite smells: testing legacy code

0