Why Go and Rust are Competitors

Caleb Doxsey

This is a post in response to http://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors.

It's a popular idea that Go is a programming language suitable for a particular set of problems (web services, command line applications) but not others (games, embedded development) where a language like Rust or C++ would be more appropriate. I think this notion is misguided and I will attempt to explain why in this post.

Go is Evolving

First its important to distinguish between Go as it exists in its current form and Go as it may exist in the (perhaps near) future. For example at one time Go wasn't a particularly good language for Windows development:

Go compilers support two operating systems (Linux, Mac OS X) and three instruction sets. http://web.archive.org/web/20100310145452/http://golang.org/doc/install.html

And now Go works great in Windows. In fact Go is one of the most capable cross-platform platforms - as easy to use as Java without the need for an installed runtime.

A more recent example is mobile development. With 1.5 it is now possible to create cross-platform (Android + iOS) native mobile applications, albeit with limited API support. Mobile development was not part of the original goal for the language, but now it stands ready to do something which very few other languages have been able to achieve - work equally well on Android and iOS.

So Go is a language that has proven itself more than capable of expanding its scope - crucially without compromising its mission. When considering its competitiveness we have to keep this in mind and ask slightly different questions: Is there something about Go's fundamental design and vision that prevents it from being appropriate for a particular set of problems where Rust would make more sense?

Control

Go and Rust embody two very different methodologies about how to write software. Go has a runtime which manages memory and handles task scheduling. Because of this the language can be kept quite small and its type system is minimalistic: structured types, with interfaces for dynamic dispatch.

Rust does not have a runtime:

Requiring a runtime limits the utility of the language, and makes it undeserving of the title "systems language". All Rust code should need to run is a stack. The Rust Design FAQ

Instead the language focuses on enforcing memory safety through its type system. Rust programmers (like C or C++ programmers) are required to think carefully about the allocation and deallocation of memory. (consider Borrow) Similarly thread management and scheduling is manual. Notably Rust removed lightweight thread support because they couldn't get it to perform well.

This difference is fundamental, but the superiority of the Rust approach should not be granted by Go programmers. Its not at all obvious that managing your own memory and scheduling your own tasks always leads to superior performance.

In fact, for a certain set of problems, it will almost certainly lead to worse performance. For example consider a web server. A web server fields many requests by reading the HTTP payload, performing some work (perhaps hitting a database or compiling a template) and then writing it back out again. As programmers we tend to think of performance for a program like this in terms of its raw components: can we speed up reading and writing the HTTP payload, can I use less memory when pulling data out of the database, etc...? By making each request as fast and small as possible we can make a faster web server.

But this gets things exactly backwards. The chief problem for modern programmers is not getting our program as small as possible, rather it's the full utilization of the resources available to us. You have a server with a vast ocean of memory and dozens of cores. Are you using these resources?

That small, fast version of the program you put so much effort into optimizing may indeed server a single request very quickly, but a web server is intended to handle multiple requests concurrently. You'd be better off using more threads and memory to perform more requests at the same time:

|-request-1-|-request-2-|-request-3-| vs |-----request-1-----| |-----request-2-----| |-----request-3-----|

Unfortunately task scheduling is hard and once you start going down this route you realize that you'd really like a unified approach to the problem. If I use epoll for my HTTP sockets, I probably also want to use it for file or database access. But how do you construct a standard library that allows you to do this?

Before long you will find yourself recreating everything that Go provides out of the box.

Problem Domains

So perhaps Rust isn't well suited for massively scaled web servers, but what about other problem domains? Transforming a simple program into one that fully utilizes the resources on your computer is difficult. Is going in the other direction just as hard?

I don't think so.

First of all the need for a truly simple program is not nearly as great as many people imagine it. One of the most successful games in history (Minecraft) was written in Java - a language not exactly known for its simplicity or small memory footprint. Many games increasingly utilize a hybrid approach to their design: a game engine written in C++, but most of the actual game code written in a higher level language like Lua.

Really the issue here isn't that Go is unsuited for this task - any complex game engine will involve similar resource utilization problems, and Go does provide the levers needed to reach levels below the language - the real issue is one of tooling. Companies use these game engines because they already work. Rolling a brand new one would be a massive investment, but its not impossible.

And Go has gotten better at playing well with other platforms. What would be wrong with combining Go with the game engine instead of a scripting language?

But even the smallest platforms aren't out of reach. With the changes in 1.5 Go is staged to provide much more flexibility in how its runtime is implemented. The garbage collector is already much more friendly to realtime applications, but perhaps Go could include a pluggable or tweakable garbage collector and task scheduler which makes sense for the particular platform you are targeting.

In other words, in its current form Go may not appropriate for these platforms, but its not because the methodology is flawed, its because the compiler and runtime technology is not yet intelligent enough. But in the future it may be - and given recent history - it seems reasonable to expect it to happen.

Computers Aren't Simple

There's also another side to this debate which is often lost in the discussion. The decisions Go makes are already decisions that were made a long time ago by the operating system. Rust may not have a garbage collector or task scheduler but Linux does. All of your memory access is virtual and the operating system has to keep track of that memory. (who owns which segments, when they can be freed, etc...) In the same way everyone already runs their software on an operating system which implements thread preemption at least at the process level. You're getting a sophisticated task scheduler merely by running your program in Linux.

The truth is that the battle Rust is fighting was lost a long time ago. Intelligence is deeply embedded throughout the whole stack. Optimizing a simple computer was hard enough when computers were dumb (when they largely matched their abstraction), but optimizing a smart computer is nearly hopeless when it was designed to make typical ways of operating fast.

Consider RAM, theoretically "random access" but in practice, multi-level caching means nearby access is faster than far away access. Or SSDs where their controller already implements log-structured storage at the controller level.

There are many more examples, but the point is that with each additional smart layer, optimization becomes much more difficult - and therefore you are better off using the simplified abstraction and letting the compiler and runtime worry about the messy details.

Rust and Go do Compete

Contrary to popular belief Go and Rust already compete in many problem domains. Rust was created as a language to implement a web rendering engine. There's no good reason Go couldn't be used for this task, and Go programmers shouldn't cede this claim.

The languages are radically different in design - they embody different approaches to writing software. This divide is not domain specific - there aren't a set of problems where Go makes sense, and a different set of problems where Rust makes sense. Rather the divide is philosophical (or ideological) and for that reason probably intractable.

Like many programmers I try to view the world in clear, black and white rationality. I see a problem, see a solution, and have a difficult time imagining how someone could come up with such a radically different solution to the same problem.

One way around this is to imagine that perhaps the reason for the different solutions was because my problem was distinctly different from someone else's. In our case perhaps I was writing a web app, and someone else was building an embedded app. But this line of thinking is a trap.

Our differences may be rooted in different circumstances, but they don't have to be. Ideological divides just aren't that simple. Go and Rust overlap and compete, both sides of the debate have rational proponents, and there's no obvious, irrefutable, objective argument for the superiority of one over the other. The debate between the two (like any philosophical debate) is worth having but we shouldn't pretend like both sides of the debate actually agree.

tl;dr

Programming is hard and Go makes it easier. The trade-off for this ease of use is loss of control. At the moment there is some cost for that trade-off in terms of performance - a cost that seems entirely worth it to me. In the long run a combination of Moore's law and compiler and runtime improvements will see that cost diminish. Perhaps one day the control deemed so necessary by C++ developers will seem as quaint as the techniques needed to write software for early consoles.