The title may be hyperbolic, but I’d like to tell a story that illustrates the assertion.
I’m the original author (but not the current maintainer) of one of the Ruby standard library packages. I’d gotten deep into Ruby when it was still relatively young. I’d also previously started using Java when it was still called Oak and had watched with some dismay as it had swollen from a relatively simple, C-like language with a minimal core library into the almost unrecognizable language it is today. Ruby’s stdlib may not have been small, but the language and libraries were intuitive. I didn’t notice the size, because I rarely had to look anything up. It was, comparatively, a straightforward language.
Around the time I was playing with the third refactoring of my library, someone posted a bug – a severe bug – where library would hang on some given input. To make a long story short(er), I tracked it down to something that was part of how Ruby managed memory, and it wasn’t something I could work around. To fix it, I’d have to fundamentally redesign the library. I’d done major refactorings before, but this would have left nothing but the API intact. I was stumped.
At around the same time, I was experiencing another with Ruby that were wearing me down. I’d been using it long enough that I had a number of server process and scripts running on homelab servers that I’d written years before, and I was becoming uncomfortably familiar with the fear of upgrading my server software. The knowledge that, if I upgraded any software, there was a high likelyhood that I’d have to spend the next few days discovering services or scheduled jobs of mine that had stopped working because of backwards-incompatible changes in Ruby dependencies.
I’ll admit that, initially, I wasn’t a fan of Go’s static compilation. Not using shared libraries seemed dumb, and the binary sizes and correspondingly larger memory use was distasteful. However, at this point I was entirely disenchanted with virtual machines and interpreters that maximized the potential for runtime incompatibility errors, and static compilation and minimized runtime dependencies seemed like a nice change. That was about 10 years ago.
So we come to today. About a week ago, a family member came to me with a request for help with a website that wasn’t working. A few years ago they’d tried to start a business; it hadn’t taken off, and they’d shelved it to focus on other parts of their life. They recently decided to revisit the scheme, and had found that the site was down, and could I help?
They’d paid a developer to write a web application; I was involved to the extent of helping them vet developers and the proposed design, so I was familiar with the basics: the developer had chosen Ruby on Rails hosted on Heroku, which I thought was a decent choice for a proof-of-concept. It was reasonably inexpensive, and if the business had taken off and made money, they could have paid for a rewrite in something else. With some trepidation, I started digging into the problem.
The first issue was simple: the web app had eaten all the DB connections and wasn’t letting them go. For a brief moment, I thought I might have had some luck; I restarted the web app, and the DB showed a free connection pool. However, the site was still down and returning 5xx errors. It turns out that, at some point, Heroku had upgraded the database software, and the version of the DB library was incompatible. I was in the deepest circle of dependency hell. Upgrading the DB library required upgrading the entire dependency tree, right up to the version of Ruby.
Four days later, working on this pretty much full time, and I still don’t have a
working stack. Granted, much of that is because I’m also learning an entire
ecosystem of technologies upon which this house of cards was built: Rails, rake,
bundler, and Heroku are all new to me. However, the amount of API churn is
surprising even given my past experience. Ruby library developers appear to love
breaking their APIs, and that’s a problem when your project
1,765 lines long.
So now we get to the point of this whole post. Go’s compatability guarantee is a great thing not only because, on the surface, Go developers can rely on their compiler updates not requiring them to constantly update their code, but because it filters down into the community. It sets a precident, a good example for developers to be thoughtful about their interfaces and reluctant to change them. Even in an ecosystem where static binaries mean that rarely needing to recompile sourcecode, the confidence that should you need to that you probably won’t spend days rewriting code is a wonderful thing.
The Go compatability guarantee is easily overlooked. It’s value stands out in stark contrast when you have to work in an environment that embraces instability and API chaos.
I wrote another blog about this in 2017 gemini, but I wanted to tell the whole story about what happened with REXML, why I dropped out of the Ruby scene, and why – after years – I’m still a fan of Go.