Stephan Schmidt

Why We Chose Go over Rust for Inkmi

Go is it for our next startup

We are building Inkmi, the first website to create dream jobs for CTOs.

Our first startup, twenty years ago, was done with Java, because all founders were Java devs. And although Java has changed and is a great language we hear, it isn’t very appealing. Our second startup, ten years ago, was with Scala and Typescript. Startup got traction, scaled, was sold, so the language choice worked. But Scala types in libraries are much too difficult, every library is its own DSL, compilation times are atrocious, and the community toxic. Typescript with its JS ecosystem has too many frameworks going in and out of fashion for our liking—but TS is a fine language.

To our current endeavor. We started in Rust because we were comfortable using Rust, but for various reasons restarted in Go.

Rust and Go are very similar and occupy similar niches. Both compile to binaries that are easy to deploy and to manage with Systemd. Instead of large deployment artifacts, deployments are syncing a binary. Just remember to use rsync not scp. The binary gets notified when it changes on disc and exists. Systemd then restarts the app with the new binary. Easy and simple. Go has the added benefit of Embedfs. This allows you to compile CSS, HTML templates, JS and images inside the binary for even easier deployments (Googling there seems to be a macro in Rust to do this).

But why Go over Rust?

Rust, the good parts

Rust is an excellent language with a sound type system. It tries to do something new for mainstream languages: make sure every piece of (mutable) data is only owned by one piece of code at a time. As a side effect it gets rid of manual memory management without the need for an (unpredictable) GC. Another side effect is that method parameters need to be declared mutable if they change (and &mut self)- something that makes understanding and error-proofing methods much easier—and we’d wished every language had.

Rust has excellent error handling, the Rust community has created several libraries for error handling and the ideas of those made it back into main Rust (Today Zig seems to strike the best balance between simple types and type based error handling though). One point in favor of Rust in error handling beside the type system is ! for fast exiting a method with an error that occurred. Rust also probably has the best compiler error messages.

Rust has the best tool chain, too. Tool upgrades just work with rustup, the toolchain feels complete. And we like clippy. And although check is due to the long compile times, we’d like to have a checker in every toolchain to speed up development (not sure go build -n is the same) if you just want to check if everything compiles - but you don’t have a need to link and run the code.

And Rust has a great, positive, supportive community.

If you go into Rust, we found the book Zero To Production In Rust most excellent.

Rust, the bad parts

If Rust is that good, why not choose it? Rust has some bad parts. First of all compilation speeds. While they get better over time, and hardware gets better, Rust compilation times are bad indeed. Macros in several libraries add to compilation time. xkcd’s “It’s compiling” is not a joke in Rust.

Second, with all the features of Rust, it has a steep learning curve. One so steep, many might not be able to climb it. If you want to hire developers without Rust experience into a Rust job, be prepared for a lot of hand holding, scaling speed and lost productivity.

The killer is the borrow checker though. To allow Rust to make sure only one piece of code owns some data, there is the borrow checker. It does this during compilation. And while this is easy for you to get it right with method calls, it gets difficult when a piece of data is created by different parts of your app. For example you construct a customer from different sources—the borrow checker will bark a lot. Rust enthusiasts will tell you to use Arc—no problem, you might end up with Arc around every field type of your classes. And while the borrow checker is great for long-running desktop applications or for IoT development, there is no big benefit for web apps (except some correctness guarantees); it’s just a pain. If you’re not a top-notch Rust developer, you often find yourself fighting the borrow checker (and loose ;-) for long stretches of time—time when you don’t write code. It gets better but it doesn’t go away.

Annoying compilation speeds, the steep learning curve for newcomers and the borrow checker that prevents productivity made us move away from Rust - a great language.

Go, the good parts

Go is very easy to understand. Not in the way of Haskell where there is no boilerplate (but the devil is in the abstracted away side effects and the big tower of stacked monad transformers). Because it is stupid simple in a good way. Go is more verbose than e.g., Ruby, it doesn’t have generators like Python, but is easy to understand when you read it. Nothing is hidden. No magic happens. When you’re through the code, you grokked it. What code is there, is there.

After several thousand lines of code you love the simplicity, the minimal amount of concepts in Go. It feels pure like C and like a higher language at the same time.

Having dependencies as git paths is nice, being able to easily have a local development version of a library with replace in go.mod is useful—although there should be a development switch; otherwise your CI breaks.

Having gofmt takes away some discussions and friction.

Finally compilation speed is a game changer—if you’re into compiled languages compared to Ruby/JS/Python that is. Go is faster to compile and start, do something and stop compared to Rust only compiling some code. You can often just use go run to run your program, and it feels like running Python. Fast turn around times make you enjoy Go and not dread compilation.

Go, the surprising parts

Go channels and goroutines are the surprising part of Go. While this was touted as the main benefit of Go by many in the beginning, it is surprisingly irrelevant when you write a web app or a library. A little bit like Java applets, the killer feature of Java which after Java became mainstream no one cared about (Goroutines are more useful for sure and you can use them to parallelize your web response - and Go seems to have an excellent scheduler that automatically puts IO on a thread).

Go, the bad parts

Go has some bad parts. Rust seems to have more libraries compared to Go. Go often feels like it is a thing of the past. You encounter a library and it is no longer maintained, people moved on from Go it seems.

The standard library of Go feels like 1980 C. Many features you have become accustomed to for application development, are not in Go. You constantly write the most basic things on your own and build up your own library. This is intentional, when asking on StackOverflow about a standard feature, like finding an element in a slice, the answer is: Go expects you to write it in five lines of code—I guess today there is ChatGPT.

The Go community feels toxic. There is no talk where the Go maintainers don’t bash another language, preferably Python and Java for some reason. This feels immature and unbalanced. The Go community is completely the opposite of the Rust community.

Documentation is bad. Many libraries are just extracted source code method descriptions—like JavaDoc in the 2000s. While this might be good enough sometimes, you often want tutorials, cookbooks or detailed explanations. Some libraries like Echo have this, but the standard is to have an inaccessible pile of extracted method comments. More often than not, you need to read the source code of your library to understand on how to use it.

The biggest risk with Golang seems to be GC. GC seems immature in Go, especially compared to Java. There are some horror stories about the Go GC out there. So we see how this works under load and hope this is not the bad part of Go.

Last not least, the error handling might be better. After some time you do appreciate if err != nil, but a short circuit operator like ! in Rust would make code more readable without too much magic and keeping the spirit of Go.

Overall, we’re happy.

But we do also understand, that the language is only a very small part of the success of a company, if at all ;-)

Other Articles

Why we always end up with waterfall

The 5 Reasons Not to Use Scrum

Our Fetish with Failover and Redundancy

Hosting a Website on BunnyCDN - Radical Simplicity

Best Books for a CTO