Innovation Awards 2022 is the market-leading awards program for celebrating ecosystem innovation and excellence across the technology sector in ASEAN.
Rust vs. Go: How to choose an enterprise programming language
- 10 March, 2021 19:45
Both languages offer indispensable features for modern software development: a sophisticated and integrated toolchain, memory safety, an open source development model, and strong communities of users.
Apart from those similarities, Rust and Go are dramatically different. They were built to scratch different itches, fulfil different demands, and write different kinds of programs.
Thus, comparing Rust and Go isn’t about which language is “objectively better,” but about which language is best for a given programming task. With that in mind, let’s look at the main ways Rust and Go differ, and the kinds of work each is best suited for.
Rust vs. Go: Performance
On the list of Rust’s major advantages, performance ranks right at the top with safety and ease, and may be the number-one item. Rust programs are designed to run at or near the speed of C and C++, thanks to Rust’s zero-cost runtime abstractions for memory handling and processing.
It’s always possible to write a slow Rust program, but at least you can be sure that Rust is not preemptively sacrificing performance for safety or convenience. What Rust does cost is an effort on the part of the developer to learn and master the language’s abstractions for memory management. More on memory management below.
Go, by contrast, does trade some runtime speed for developer convenience. Memory management is handled by the Go runtime (again, more below), so there is an inevitable amount of runtime-associated overhead. But for many scenarios, this trade-off is negligible.
Go is by default many times faster than other languages of convenience, such as Python, at the slight cost to the programmer of requiring strong types for all objects. Python’s convenience and flexibility come at a significant performance cost.
In short, Rust is faster overall, but for most workaday use cases the difference in speed between Rust and Go will be marginal. In cases where performance is an absolute requirement, Rust can excel in ways that Go cannot.
Rust vs. Go: Memory management
Memory management in Rust and Go are strongly related to the performance behaviours in both languages.
Rust uses compile-time ownership strategies for memory management by way of zero-cost abstractions. This means the vast majority of memory management issues can be caught before a Rust program ever goes live.
If a Rust program isn’t memory safe, then it doesn’t compile. Given how much of our world is built atop software that is routinely found to be unsafe due to bad memory management, the Rust approach is way overdue.
As noted above, this safety comes at the cost of Rust’s more challenging learning curve: Programmers must know how to use Rust’s memory management idioms properly, and that requires time and practice. Developers coming from the worlds of C, C++, C#, or Java must rethink how they write code when they sit down with Rust.
Note that it would be possible to introduce garbage collection into Rust separately, as a third-party, project-specific addition. One such project, the gc crate, already exists, although it’s still considered primordial. But Rust’s underlying paradigms don’t use garbage collection.
Like Rust, Go is memory safe, but that’s because memory management is handled automatically at runtime. Programmers can write thousands of lines of Go code and never once have to think about allocating or releasing memory.
Programmers do have some control over the garbage collector at runtime. You can change garbage collection thresholds or trigger collections manually to prevent garbage collection cycles from interfering with operations.
Programmers can also perform some manual memory management in Go, but the language deliberately stacks the deck against doing so. For instance, you can use pointers to access variables, but you can’t perform pointer arithmetic to access arbitrary areas of memory unless you use the
unsafe package, an unwise choice for software that runs in production.
If the programming task at hand requires you to allocate and release memory manually—e.g., for low-level hardware or maximum-performance scenarios—Rust is designed to accommodate those needs. You shouldn’t assume that Go’s automatic memory management disqualifies it for your task, but Rust has proven to be a better fit in certain high-performance scenarios.
In one recent example, Discord switched from Go to Rust for one of its key back-end services, partly due to issues with Go’s memory model and garbage collection. The Rust version of the service outperformed the Go version in its earliest iterations without any hand-tuning.
Rust vs. Go: Development speed
Sometimes development speed is more important than program speed. Python has made a career out of being not-the-fastest language to run, but among the fastest languages to write software in.
Go has the same appeal; its directness and simplicity make for a speedy development process. Compile times are short, and the Go runtime is faster than Python (and other interpreted, developer-friendly languages) by orders of magnitude.
In short, Go offers both simplicity and speed. So what’s missing? Some features found in other languages (e.g., generics) have been omitted to make the language easier to learn, easier to master, and easier to maintain. The downside is that some programming tasks aren’t possible without a fair amount of boilerplate.
Work is under way to add generics to Go, but that remains a work-in-progress, and switching to a new Go that uses generics would require reworking a great deal of existing code to make it newly idiomatic.
Rust has more language features than Go, and it takes longer to learn and master. Rust’s compile times also tend to be longer than equivalent Go programs, especially for applications with large dependency trees. This remains true even after a concerted effort by the Rust project to shorten compile times.
If a fast development cycle and the need to bring people on board a project quickly are top priorities, Go is the better choice. If you’re less concerned about development speed, and more concerned with memory safety and execution speed, pick Rust. In either case, if you have a team that is already deeply experienced with one of the two languages, lean towards that language.
Rust vs. Go: Concurrency and parallelism
Modern hardware is multi-core, and modern applications are networked and distributed. Languages that don’t plan for these realities are behind the curve. Programmers need to be able to run tasks independently, whether on a single thread or multiple threads, and to share state between tasks without risking data corruption. Rust and Go both provide ways to do this.
Concurrency was baked into the Go language’s syntax from the beginning, by way of goroutines (lightweight threads) and channels (communication mechanisms for goroutines). These primitives make it easy to write applications (such as network services) that must handle many tasks concurrently without risking common issues like race conditions.
Go doesn’t make race conditions impossible, but provides native test mechanisms to warn the programmer if race conditions could occur at runtime.
Rust gained native concurrency syntax, in the form of the
.await keywords, with version 1.39.0 in late 2019. Before
.await, concurrency came by way of a crate or package for Rust called
Although Rust’s concurrency lacks the years of consolidated developer experience behind Go’s concurrency, it inherits the advantage of Rust’s memory safety—meaning that Rust code that could expose race conditions simply won’t compile. Concurrent or asynchronous operations will be harder to write in Rust, due to Rust’s syntax rules, but it will be durable in the long run.
Rust vs. Go: Interoperability with legacy code
New languages like Rust and Go aim for memory safety and programmer convenience in ways that earlier languages didn’t fathom. But the new always has to co-exist to some degree with the old. To that end, Rust and Go both interoperate with legacy C code, although with different restrictions in each case.
Rust can talk directly to C libraries by way of the
extern keyword and the
libc “crate” (Rust’s name for a package), but all calls to such libraries have to be tagged as unsafe. In other words, Rust can’t guarantee their memory or thread safety; you need to manually ensure that interfaces to C code are safe. Many examples of how to wrap code with Rust’s FFI (Foreign Function Interface)—for C and other languages—can be found at the Rust FFI Omnibus website.
Other projects are making use of Rust’s type system and static analysis features to create safe bridges between languages. For example, the CXX project provides safe bindings to C++ from Rust.
Go provides the
cgo package for working with C. The
cgo package allows you to call into C libraries, use C header files in your Go code, and convert common data types between C and Go (e.g., strings). However, because Go is memory-managed and garbage-collected, you have to ensure that any pointers you pass to C are handled correctly.
cgo also tends to impose per-call overhead, meaning it will always be slower than working with C directly.
In short, Rust is slightly friendlier regarding C interop than Go, so anything with major dependencies on existing C may tip the scale toward Rust. In both Rust and Go, though, interoperability with C comes at some cost to the developer: more conceptual overhead, slower compile times, more complex tooling, harder debugging.
Note that Rust and Go can always interface with other code at higher levels—e.g., exchanging data via network sockets or named pipes rather than function calls—but these alternatives come at the cost of speed.
Rust vs. Go: Summing up
Choosing between Rust and Go for a software development project is mainly about picking the language that has the qualities you need most for that project. For Rust and Go, you can sum up those qualities as follows.
- Correctness at runtime (common mistakes simply don’t compile)
- Top-tier execution speed
- Memory safety without garbage collection (or with optional, experimental garbage collection via third-party projects)
- Hardware-level code
- Quick development cycles
- High-tier execution speed
- Memory safety by way of garbage collection (with optional manual memory management)
- Developer convenience
- Straightforward code