Go and Rust

Page content

My regular job has me working, from time to time, with both Go and Rust. Both of these languages have a good following, and strong communities behind them, and there are things I enjoy about both languages.

For the most part, if I am writing a utility for personal purposes, I will choose one of these two languages (notably, not Python). Several of my projects, I find myself maintaining in both Go and Rust, because I can’t make up my mind as to which I prefer, or which is a better fit for the problem.

Two modern languages

Go (often called “golang” to make search work better) begin in 2009 by Robert Griesemer, Rob Pike, and Ken Thompson, at Google. At the time of writing, it has had 9 point releases since the 1.0 release in 2012.

Rust begin as a sponsored project by Mozilla in 2009. At this time, there have been 22 point releases since the 1.0 release in 2015. Both languages release on a regular time-based release schedule, 6-months in the case of Go, and 6-weeks in the case of Rust (hence the different number of releases).

Both languages underwent a period of significant change before the 1.0 release, but have both stabilized at that point, and for the most point, code written for 1.0 will still work in either case.

Language design is largely an effort in compromise. Rarely can a feature be added to a language without impacting other aspects of the language, or the user’s experience using it. Rust and Go set about to address fairly different problems, and as such have ended up being fairly different languages.

Rust, above everything else, strives to be a safe language. It contains features, such as the borrow checker, that are able to enforce pointer lifetime and even concurrency safety in ways that are unique to Rust.

One thing that both languages did in a similar way was to eschew the inheritance model that is prevalent in many OO languages, and instead focus on interfaces or traits (interface being the Go term, and trait being the rust term). The idea is to separate data (structures) from actions on that data (methods). Although there can be dependencies between traits, and composition can be used to combine data structures, they are independent from one another. This method tends to increase orthogonality and decrease coupling in code, which helps greatly with maintenance of programs, especially as programs become large.

Learning curve

The borrow checker, stricter typing, and in general, greater overall complexity tend to make Rust a more difficult language to learn. These advanced features, especially the borrow checker and type system, need to be understood fairly well to write even basic programs. Go, on the other hand, has made simplicity an explicit goal, which has the consequence of making the language easier to learn.

For someone coming from another programming language, there is going to be some gestalt learning to be able to effectively program in either language (especially if coming from an OO language where inheritance is emphasized). Go has a specific model of concurrency it offers that can take some learning, but this is kept orthogonal from the rest of the language, and can be postponed in learning until needed.

Although the added cost of learning Rust is significant, I want to focus the rest of this article on what the language is like for someone who has learned it. Although the borrow checker can be confusing, most programmers seem to reach a point where it “clicks”, and doesn’t become a significant hindrance to development. There are issues where some programs are just not expressible in Rust (without using unsafe), which can result in code that is more complex both to write and to understand. This will be addressed below.

Build and package management

Before diving into the languages themselves, I want to consider the module and/or package systems that they each have. Both languages try to address the problems that arise in, what I call, “programming in the huge”. The term programming in the large is often used to describe how to break a program into separate modules, or namespaces or the likes, and how to keep these parts of the system as decoupled as possible.

Many languages, for quite some time, have been offering some type of module or namespace system to keep names separate from one another (spawning from the work, in the 70’s on the appropriately named Modula language). These systems help organize and decouple the parts of a software system allowing these parts to be worked on independently, and reducing the memory load of the programmer, who doesn’t have to conceptually grasp the entire program while working on part of it.

As software grows larger, however, it starts to become necessary to combine software systems provided by diverse groups of developers to make a coherent system. When doing this, simple namespaces can result in conflicting names. Some approaches to solving this have been to use reversed domain names as part of the module path (Java), or provide a system outside of the module system that allows renaming and shadowing of names as they are combined (the SML basis system).

Go and Rust each take one of these approaches, although not exactly. In go, the division of modularization within a program is the package. Each package has a canonical name, which generally consists of a host name followed by a directory path. When using a module, the import declaration will refer to the full path, but the code then only need to refer to the last component of the path. Conflicting trailing names don’t cause any problem when they are used by different parts of the system. When used by the same part, the import directive for renaming to avoid the conflicts. Essentially, each package is built with its own view of the space of external modules.

In rust, the unit of compilation, the crate, also has its own view of the external crate namespace. There are subtle differences, such that in rust, the module names are coordinated through a package repository, but it is also possible to use local modules by path.

Early on, Rust created a dependency and build management tool, called Cargo. Cargo tracks dependencies specified by a project as well as transitive dependencies in order to pull together a coherent system.

Go has postponed development of the dependency part of this tool (it is now under development) creating a build tool go that builds code that lives in a tree mirroring the canonical names for the packages. They provide a tool to fetch these under some circumstances, but are still developing a tool to allow versions to be constrained.

Overall, Rust is further along in terms of managing dependencies. The Go community is working on this (finally). For personal development, I’ve not encountered too many problems with the current state of Go, and it has tended to encourage API stability by package authors.

Build environment

One big difference between the two languages is the expected build environment. Go expects a directory (pointed to by the GOPATH environment variable) there all of the software relevant to building will live. A package example.com/user/project must live under $GOPATH/example.com/user/project.

This tends to result in having a single tree with git repos scattered around inside of it. My projects may live under github.com/user/p1 and so on, but my dependencies may live somewhere else. Recently added vendoring support helps with this, but keeping dependencies separated from the code being worked on.

The main disadvantage I find is that this prevents me from organizing my code with what it goes with. This project-area type layout works well for projects where the code is the entirety of what the project consists of. But, I find that I often have a small Go or Rust program that is part of a larger environment. For example, the Mynewt project is an embedded real-time operating system (RTOS). Its build tooling is written in Go, and they do some fairly non-Go-like things to be able to build this tool while living in the project directory.

What I will then end up doing is having two clones of my project, one in the project work area based on the surrounding project, and then another clone within $GOPATH that I use to work on and build the tooling.

Memory management

A significant difference between Rust and Go is the approach to memory management. Rust follows a similar approach to C++ (and Ada) by doing entirely manual memory management, but by statically managing lifetimes so that there are no leaks without having to explicitly free things. This works well for any tree-shaped structure, but other structures require either reference-counted types, or changes to data structures to use things like vector indices instead of pointers.

Go, on the other hand, embraces garbage collection. There is no explicit free. Unlike most garbage collected languages, Go does not have finalizers that can be attached to objects, but instead has a defer construct that can be used to ensure a resource is closed at the end of a function.

My personal take is that a vast majority of programming problems are better served by a garbage collector than explicit memory management. For most uses, the overhead is negligible, or at least acceptable.

The availability or lack thereof, of a garbage collector has a pervasive and profound impact on the rest of the language. Although Rust has closures, they are very limited, and much more complex to use (there are 3 kinds, and the programmer has to be explicit about how variable capture happens, etc). Having them in Rust is definitely an advantage, but they are by no means as use of a construct as they are in a language with a garbage collector.

On the other hand, having a garbage collect, Go closure work much they same as would be expected by a Scheme programmer. They can reference variables in an outer scope, and those captures will survive even the exit of the function that defined them. By using escape analysis to place items in the heap as needed, there is no need to worry about lifetimes and whether a stack frame will still be valid.

Although the lack of GC is touted as an important feature of Rust, I don’t believe it has ever been helpful to me. Although I’ve programmed on memory constrained devices, these tend to not even have manual memory management. Everything else I’ve written has benefitted greatly by having a garbage collector.

I consider this a significant advantage of Go, and I feel that Rust has restricted itself to a problem domain that isn’t very common.

Type strictness

One of Go’s design goals is explicitly simplicity. As such, its type system is fairly simple. It has only product types composed of basic types. It has a basic array and map type that, themselves, are generic, but no mechanism to generically use these types. Problems whose needs go beyond this tend to result in using dynamic typing, at least for that part. The language has an interface{} type which can hold any object that has at least zero methods (so, anything). There are ways to dynamically cast back to concrete types, so this can be used for general data structures, but at a slightly performance cost.

Go also has, and uses reflection. Most instances that work generically (encoding, for example) use reflection to traverse data structures. This comes at a bit of a performance hit, but keeps the type system much more simple. In contrast, Rust tends to try and generate this kind of code at compile time. As such, Rust has a richer ecosystem of compile-time plugins to derive specialized code for user-defined types.

I do find myself, when writing go, at times wishing I had a richer type system. Having proper sum types seems much cleaner than using multiple return values for error handling (and having to rely on everything having a zero value). I have found, though, enough articles and blog posts about this to at least start giving Go a chance in this area. The argument is that the tradeoff of a bit more complexity in the small keeps much of the rest of the language simple, reducing complexity in the large.

Generics

Lastly, is the issue of generics. Rust embraces generics, both at a type, and at a template level (like C++). This can often result in slow compiles and large executables, but the code will often be faster.

Go is punting on generics until at least a 2.0 version of the language, and even then they aren’t promising anything. Not having Generics in Go, for me, mostly results in having to write out a lot of boilerplate code. Rather than being able to use simple methods to extract the keys of a map, sort them, etc., I write small loops that do these operations. Finding a way to support these without hurting the simplicity of the rest of the language would be a big win, in my mind.

Conclusion.

Overall, I find my self a little bit more productive with Go than I do with Rust. However, I do feel safer about the code I’ve written in Rust, knowing that many cases of null pointers will have been caught at compile time. I find, sometimes, that Go’s weaker type system is less helpful when doing a complex refactoring (still much more helpful than a dynamic language, though).