Rust and Go
- Ruby feels like it always hits the “whipuptitude” part of my brain. It’s easy to simply sit down and start typing, with very little in the way. It also has the expressiveness that I always loved in Perl. The more you understand the language, the more it feels like I can express myself in the same way I do with English.
- Erlang and OTP are glorious to operate. Things like pattern matching, actor concurrency, single assignment, and a lovely runtime make it a joy to run, manage, and debug production services. I think the syntax is awkward, but it too has a terse kind of beauty when you soak in it.
So — I decided to write a little Rust and, because everyone in my world seems swoony over it, Go.
I started paying attention to Rust a couple of months ago. The language is designed to fill the same niche as C/C++ — suitable for low level programming, with high safety guarantees, and a novel approach to memory management (borrowing). It also has a bunch of language features that I love from Erlang — pattern matching, actor style concurrency, immutable variables.
My history with C/C++ is limited — to be honest, my history with any strongly typed language is limited. As a systems administrator, there just was rarely a reason to do more than patch someone elses code (which I’ve done plenty of — but a patch isn’t the same as writing something from scratch.) In the few experiences I’ve had, I found the compile->run->segfault loop to be deeply irritating. I recognize that this is my own ignorance on display: if I spent more time in the language, I would certainly come to understand the pitfalls that were so regularly striking me.
So it was that I set out to re-write a command line utility from Ruby to Rust. I have no intention of shipping it, or even really sharing it — it was just an excuse to learn the language. Here is what I observed:
Cargo is nice
Cargo as the front-end tool to Rust projects is coming along super nicely. Creating a new Rust project, adding in testing, dependencies, etc was easy and super clear.
Expressive like a scripting language
Rust can be quite expressive in a way that feels like a low level scripting language. Check this out:
While things like as_slice() and Vec<String> might take a little explaining, but it’s remarkably straightforward. Here is the review function:
Again, I don’t think this is any more difficult than it would have been in a scripting language. It takes a little getting used to the fact that, for example, I return an Option<Path> from have_dot_git(cwd), and use pattern matching to extract the value (or raise an error) — but once you realize whats going on under the hood, it’s pretty great.
In every other language that was strongly typed, I always felt like I was in a wrestling match with the compiler. I found it exceedingly difficult to trust that it was stopping me from doing something stupid, rather than getting in my way. (I recognize this is an emotional flaw of my own) Rust felt different for three reasons:
- Its error messages are exceedingly clear, pointing you directly to what is going wrong, and often showing you the precise solution. (“Did you mean…”)
- The compile loop was very fast, and when my code compiled successfully, it always ran exactly like I wanted it to.
- The idea of borrowing, and the idea of lifetimes, takes some getting used to. However, the compiler really is acting in your best interest all the time, and again the error messages and rules are clear. There is no traditional garbage collection in Rust — but it has very clear rules for how long things on the stack and heap will live. Those rules tie closely to scoping, in a way that feels really natural.
Most importantly, for me, my code never segfaulted or panicked. This was an experience that I’ve never had in my life when working with a low level systems programming language. If the compiler accepted my input, it ran — fast and correctly. Period.
Libraries don’t really exist
Rust is still evolving, heading towards a 1.0 release. As a side effect, a lot of the higher level libraries just don’t exist, or haven’t been written. A good (trivial) example was a great command parsing library just doesn’t exist yet. There is one included in the standard library, but the interface is clunky at best. A few promising libraries existed on Github, but the best one (from an interface/functionality point of view) failed to compile on the latest version of Rust. (This was due to the fail!(..) macro being renamed to panic!(..) — not a big deal, but a sign of the kind of things that are still evolving.)
Similarly, Cargo is great at fetching dependencies — but there isn’t a collection of what is available, a-la NPM, Rubygems, or CPAN.
Final Thoughts on Rust
Rust is a powerful, deeply well thought out systems programming language with a pretty amazing and useful set of features. (Generics, Traits, Pattern Matching, Actor concurrency, Borrowing — the list goes on) There is a tax on learning the surface area and “Rust Way” of solving a problem — the language itself is simple, but has lots of different concepts to wrestle with.
That said, its the first time in my life I feel like I could trust myself to write fast, efficient, low level systems code. The compiler was your buddy, and the errors it fed me were useful and clear. When the language hits 1.0, and the ecosystem of libraries starts to heat up — Rust is going to be a force to be reckoned with.
Lots of people have said lots of things about how amazing Go is. After spending even a few hours re-building something in the language, I see the appeal.
Go is small
You can learn the surface area of the language super quickly, assuming you have ever programmed in anything C derived. There are pretty much only the things you already know how to use in the language: if/then/else, switch, for. They use the syntax you already know. They provide short hand for things that you could type verbosely, but don’t want to (:=, for example).
Go is opinionated — about lots of things
When you read How to Write Go Code it starts with telling you how to lay out the top level source directory for everything you will write in Go. The Go tooling knows how to take that structure and do the right thing with it — compile binaries that go into your ‘bin’ directory, run your tests, grab dependencies.
The static output is nice — and the reality that, whatever you compiled locally will be what your users consume feels great.
Go gets out of your way
Where the Rust compiler made sure that what I expressed would result in working low level code, Go sits in the middle. The language is strongly typed, but in practice it seems to infer what you mean most of the time. For example:
The only type annotations in that function are the inputs and the outputs — everything else just works. It feels fast, easy, approachable and very low hassle.
Go failed at runtime
It was very, very hard to get Rust to fail at runtime. The compiler made sure I checked every code path, always made sure I covered every angle. Go was more than happy to do what I told it — while the compiler would stop me from being egregiously stupid, it wasn’t going to stop me from doing something like this:
Where line 9 there just blindly assumes the regex found a match, and causes quite the run-time error message. This was impossible in Rust:
Because the return of my regex capture as an Option, the compiler knew I needed to deal with all the possible values. I literally couldn’t choose to ignore it. This happened when I wrote the code — in Go, I just wrote what I thought I wanted, then learned about my mistake at runtime (and it’s a trivial fix.) In Rust, the compiler told me when I first tried the function that I hadn’t dealt with the None result of my Option<Capture> return.
Speed, accessibility, and libraries
The trade-off is interesting. The Go code took far less time to write, the language was so accessible, and the ecosystem was strong. It let me work like I would have in a scripting language, while stopping the most egregious kinds of errors. Because the only language constructs are the most common constructs, it took no time at all to feel facile in the language.
Go had a huge library ecosystem, a clear way to find them, and an easy way to get them.
So.. Rust or Go?
The pragmatic answer is ‘horses for courses’, I guess. Go could clearly fill the same niche that people write Ruby, Python, or Perl for. It’s an easy, approachable language with a strong and fabulous ecosystem. I see why people love it. Static binaries, great ecosystem. Kind of a layup. I wrote the Go code probably twice as fast as as the Rust code.
The thing is, I liked Rust more. I like that it has pattern matching, generics, immutable data by default. I liked that if my code compiled at all, the odds that it did what I wanted were extraordinarily high. When I think about what an ecosystem built on those fundamentals would be like, I want to live in that world. It’s better than the world I live in on languages I already have an affinity for for a wide range of use cases, and lets me play in a space I haven’t really ever felt comfortable in. I’m pretty sure the advice that Rust is amazing where you would use C/C++, and Go is amazing where you would use Ruby or Python is true. However, I think once Rusts ecosystem takes off, it will start being a great choice for the Ruby/Python niche as well.
I was reminded of a lesson I learned about executive hiring — rule number one is that they have to be amazing at something. Someone who is just “good” at everything won’t, in general, actually turn out to be great. Go felt that way to me — it was good at everything, but nothing grabbed me and made me feel excited in a way I wasn’t already about something else in my ecosystem. Rust is absolutely amazing at ensuring the code you write is correct and safe at runtime — it’s a revelation.
You should definitely give both a try.