Stratus3D

A blog on software engineering by Trevor Brown

Why I Chose Go

I’ve written a couple of blog posts this year on the rewrite of asdf in Go. My previous posts have covered some of the technical details of the rewrite. I thought I spent quite a bit of time weighing the merits of several different languages before I settled on Go. I’d take some time to share my reasons for choosing Go.

Goals

The choice of programming language is largely determined by the constraints and goals of a project. For this rewrite my goals were to improve the performance of asdf and make the codebase more maintainable. The biggest constraint on the rewrite work was my very limited time. The rewrite was one of my side projects so I could only dedicate a couple of hours a week to it. I was open to learning a new programming language, but it had to be one that I could be productive in fairly quickly.

Elixir or Erlang

If you’ve followed my blog you know I’m a fan of Erlang and Elixir, and I’ve been writing software for the Erlang VM for over a decade. So why not use Elixir or Erlang? There are tools like Bakeware that make it easy to build self-contained binaries from an Erlang/Elixir application. While it would be possible to rewrite asdf in Elixir it would have been a bad choice for several reasons:

  • asdf commands need to run quickly. The Erlang VM was not optimized for fast boot up. The Erlang VM can easily take several hundred milliseconds to start up, and we need something that would only take a few milliseconds at most to run.

  • Erlang and Elixir are seldom used for CLI tools. Using them would result in an esoteric application with source code that would only be understood by a relatively small group of people. Not a great choice for an open-source project.

  • Most other maintainers didn’t have experience with Erlang/Elixir. This would mean current maintainers would have to learn the basics of the Erlang VM and an entirely new and unique syntax, regardless of whether I used Erlang or Elixir.

While I love Erlang and Elixir they clearly would have been a poor choice for asdf.

Rust

Rust was the first language I considered when I started down the path of rewriting asdf. I was very interested in Rust’s approach to memory management, and was pleased with the command-line tools I’d been using that were written in Rust. The binaries it produced were small and fast. Rust also seemed to be a popular choice for open-source software. It seemed like a perfect fit for a command-line application.

I built a couple command-line tools in Rust as I was learning it. Then began rewriting asdf in Rust. Shortly after beginning the rewrite I abandoned it because the development experience was terrible for me. The Rust compiler was extremely slow and the small command-line applications I’d created took almost two minutes to compile on my laptop. Even cargo check took over thirty seconds to run. Another issue was the cognitive load placed on me by the Rust compiler. Writing anything at all in Rust took a lot of thought. This compounded with the slow compiler to create a very unpleasant experience. While working on new code I’d inevitably forget something, wait a while for the compiler to run, then find out I’d made a mistake.

Even after writing a couple of simple CLI apps in Rust everything I did in it just felt difficult and slow. The slowness could have been improved by purchasing a faster computer and I could have dedicating more time to learning the language so I’d make fewer mistakes. But ultimately it wasn’t what I was looking for. I was looking for a language that would be another small tool in my toolbelt, not something that would require purchasing new hardware and dedicating a year or more of free time to learn.

Nim, D, Vlang

After abandoning Rust I started evaluating more niche languages. Most of them I didn’t dedicate much time to, but I’m mentioning them here for completeness.

D is an older language with some really interesting properties. Its ability to run without a garage collector interested me. It seems pretty solid and could have performed very well. It is a fairly obscure programming language today with few developers using it. I didn’t feel like it was worth the effort to learn D and try to build a prototype in it.

The creator of Nim tried to take the best of D, but also has some unusual ideas that I believe have hobbled the language. The most visible being how identifiers work. In most languages identifier (like function and variable names) must match exactly. If you want to invoke the function hello you must write hello(). In Nim, it’s more complicated. Two identifiers are considered equal if, after removing underscores, the first characters are an exact match, and all subsequent characters match when converted to lowercase ASCII. For example, he_llo, hELLO, and hello would all be considered the same identifier. This means tools like grep can’t be used on Nim codebases easily. Even a search for a simple function name like hello would require creating some sophisticated regex to find all uses of an identifier. Not being able to grep for all invocations of a function felt like a terrible design. Because of this I couldn’t bring myself to use the language.

I spent a little time playing with V. I found that it had unfixed compiler bugs that had been reported several years earlier. The Vlang website makes some false claims, and after getting the compiler to crash on some simple code, I gave up on it. Ultimately Nim and Vlang were a waste of my time.

Go

After considering all these options I decided to try Go. I’d used Go once before, shortly after it was first released, and at the time, I ended up liking Elixir more and set Go aside. But for this project I decided to re-evaluate Go. The compiler is fast and the language is simple. Go code is easy to reason about and fast to compile. The feedback loop with the fast compiler is incredible. There are some features from Rust I’d love to see in Go, but from a productivity standpoint, Rust can’t compete with Go. Go feels fast and pragmatic. I’m still not a fan of having to use the if err != nil { return err } pattern everywhere in my code, but the productivity boost more than makes up for the verbose error handling syntax.

I ported the programs I’d written in Rust over to Go and then started rewriting asdf in Go. It felt like the right choice for asdf for a number of reasons:

  • Go programs are typically distributed as binaries. This makes installing as easy as downloading the appropriate binary.

  • Go programs are fast, with performance comparable to the fastest garbage-collected languages. It would be fast enough for asdf.

  • Go is very popular for open-source; even developers who don’t know Go can usually understand enough to locate bugs and submit simple patches.

  • Several of the asdf maintainers already knew Go.

Conclusion

The rewrite in Go still took almost a year to complete. The timeline for the rewrite was this:

  • 2/2/2024 - Started working on Golang implementation

  • 11/22/2024 - Started dogfooding the new asdf implementation

  • 12/14/2024 - Feature parity

  • 12/15/2024 - Go code merged to master

  • 1/30/025 - Go version released as asdf 0.16.0

The rewrite took longer than I expected, but that was due to other constraints and not the choice of language. Translating the codebase into Go progressed at a steady pace with few surprises along the way. The code I wrote was so predictable and straightforward that it almost felt boring to write. It was just the steady process of using TDD to turn out simple Go code, one feature after another.

asdf, go, programming

Get more articles like this in your inbox

If you enjoyed this article and would like to receive more articles like this subscribe to my newsletter via email or RSS. I won't send you more than one email a month.

« asdf Performance Improvements