In the two years since I’ve posted I want off Mr Golang’s Wild
Ride, it’s made the rounds time and
time again, on Reddit, on Lobste.rs, on HackerNews, and elsewhere.
And every time, it elicits the same responses:
- You talk about Windows: that’s not what Go is good at! (Also, who cares?)
- This is very one-sided: you’re not talking about the good sides of Go!
- You don’t understand the compromises Go makes.
- Large companies use Go, so it can’t be that bad!
- Modelling problems “correctly” is too costly, so caring about correctness is moot.
- Correctness is a spectrum, Go lets you trade some for development speed.
- Your go-to is Rust, which also has shortcomings, so your argument is invalid.
- etc.
There’s also a vocal portion of commenters who wholeheartedly agree with the
rant, but let’s focus on unpacking the apparent conflict here.
I’ll first spend a short amount of time pointing out clearly disingenuous
arguments, to get them out of the way, and then I’ll move on to the fairer
comments, addressing them as best I can.
The author is a platypus
When you don’t want to hear something, one easy way to not have to think about
it at all is to convince yourself that whoever is saying it is incompetent, or
that they have ulterior motives.
For example, the top comment on HackerNews right now starts like this:
The author fundamentally misunderstands language design.
As an impostor syndrome enthusiast, I would normally be sympathetic to such
comments. However, it is a lazy and dismissive way to consider any sort of
feedback.
It doesn’t take much skill to notice a problem.
In fact, as developers get more and more senior, they tend to ignore more and
more problems, because they’ve gotten so used to it. That’s the way it’s always
been done, and they’ve learned to live with them, so they’ve stopped questioning
it any more.
Junior developers however, get to look at everything again with a fresh pair of
eyes: they haven’t learned to ignore all the quirks yet, so it feels
uncomfortable to them, and they tend to question it (if they’re made to feel
safe enough to voice their concerns).
This alone is an extremely compelling reason to hire junior developers, which I
wish more companies would do, instead of banking on the fact that “seniors can
get up-to-speed with our current mess faster”.
As it happens, I am not a junior developer, far from it. Some way or another,
over the past 12 years, seven different companies have found an excuse to pay me
enough money to cover rent and then some.
I did, in fact, design a language all the way back in
2009 (when I was a wee programmer baby), focused mainly on syntactic sugar
over C. At the time it was deemed interesting enough to warrant an invitation to
OSCON (my first time in Portland Oregon, the capital of grunge, coffee, poor
weather and whiteness), where I got to meet other young and not-so-young
whippersnappers (working on Io, Ioke, Wren, JRuby, Clojure, D, Go, etc.)
It was a very interesting conference: I’m still deeply ashamed by the
presentation I gave, but I remember fondly the time an audience member asked the
Go team “why did you choose to ignore any research about type systems since the
1970s”? I didn’t fully understand the implications at the time, but I sure do
now.
I have since thoroughly lost interest in my language, because I’ve started
caring about semantics a lot more than syntax, which is why I also haven’t
looked at Zig, Nim, Odin, etc: I am no longer interested in “a better C”.
But all of that is completely irrelevant. It doesn’t matter who points out that
“maybe we shouldn’t hit ourselves in the head with a rake repeatedly”: that
feedback ought to be taken under advisement no matter who it comes from.
Mom smokes, so it’s probably okay
One of the least effective way to shop for technologies (which CTOs, VPs of
engineering, principals, senior staff and staff engineers need to do regularly)
is to look at what other companies are using.
It is a great way to discover technologies to evaluate (that or checking
ThoughtWorks’ Tech Radar), but it’s far
from enough.
A piece from company X on “how they used technology Y”, will very rarely
reflect the true cost of adopting that technology. By the point the engineers
behind the post have been bullied into filling out the company’s tech blog after
months of an uphill battle, the decision has been made, and there’s no going
back.
This kind of blog doesn’t lend itself to coming out and admitting that mistakes
were made. It’s supposed to make the company look good. It’s supposed to attract
new hires. It’s supposed to help us stay relevant.
Typically, scathing indictments of technologies come from individuals, who
have simply decided that they, as a person, can afford making a lot of people
angry. Companies typically cannot.
There are some exceptions: Tailscale‘s blog is
refreshingly candid, for example. But when reading articles like netaddr.IP: a
new IP address type for
Go, or Hey linker, can
you spare a meg? you can react in
different ways.
You can be impressed, that very smart folks are using Go, right now, and that
they have gone all the way to Davy Jones’ Locker and back to solve complex
problems that ultimately helps deliver value to customers.
Or you can be horrified, as you realize that those complex problems only exist
because Go is being used. Those complex problems would not exist in other
languages, not even in C, which I can definitely not be accused of shilling for
(and would not recommend as a Go replacement).
A lot of the pain in the netaddr.IP
article is caused by:
- Go not having sum types — making it really awkward to have a type that is
“either an IPv4 address or an IPv6 address” - Go choosing which data structures you need — in this case, it’s the
one-size-fits-all slice, for which you pay 24 bytes on 64-bit machines. - Go not letting you do operator overloading, harkening back to the Java days
wherea == b
isn’t the same asa.equals(b)
- Go’s lack of support for immutable data — the only way to prevent something
from being mutated is to only hand out copies of it, and to be very careful
to not mutate it in the code that actually has access to the inner bits. - Go’s unwillingness to let you make an opaque “newtype”. The only way to do
it is to make a separate package and use interfaces for indirection, which is
costly and awkward.
Unless you’re out for confirmation bias, that whole article is a very compelling
argument against using Go for that specific problem.
And yet Tailscale is using it. Are they wrong? Not necessarily! Because their
team is made up of a bunch of Go experts. As evidenced by the other article,
about the Go linker.
Because they’re Go experts, they know the cost of using Go upfront, and they’re
equipped to make the decision whether or not it’s worth it. They know how Go
works deep down (something Go marketing pinky-swears you never need to worry
about, why do you ask?), so if they hit edge cases, they can dive into it, fix
it, and wait for their fix to be upstreamed (if ever).
But chances are, this is not you. This is not your org. You are not Google
either, and you cannot afford to build a whole new type system on top of Go just
to make your project (Kubernetes) work at all.
The good parts
But okay – Tailscale’s usage of Go is pretty out there still. Just like my
2020 piece about Windows raised an army of “but that’s not what Go is good for”
objections, you could dismiss Tailscale’s posts as “well that’s on you for
wanting to ship stuff on iOS / doing low-level network stuff”.
Fair enough! Okay. Let’s talk about what makes Go compelling.
Go is a pretty good async runtime, with opinionated defaults, a
state-of-the-art garbage collector with two
knobs, and tooling that would make C developers jealous, if they bothered
looking outside their bubble.
This also describes Node.js from the very start (which
is essentially libuv + V8), and I believe it also describes “modern Java”, with
APIs like NIO. Although I haven’t checked what’s happening in Java land too
closely, so if you’re looking for an easy inaccuracy to ignore this whole
article, there you go: that’s a freebie.
Because the async runtime is core to the language, it comes with tooling that
does make Rust developers jealous! I talk about it in Request coalescing in
async Rust, for example.
Go makes it easy to dump backtraces (stack traces) for all running goroutines in
a way tokio doesn’t, at this time. It is also able to
detect deadlocks, it comes with its own profiler, it seemingly lets you not
worry about the color of
functions, etc.
Go’s tooling around package management, refactoring, cross-compiling, etc., is
easy to pick up and easy to love — and certainly feels at first like a definite
improvement over the many person-hours lost to the whims of pkg-config,
autotools, CMake, etc. Until you reach some of the arbitrary limitations that
simply do not matter to the Go team, and then you’re on your own.
All those and more explains why many, including me, were originally enticed by
it: enough to write piles and piles of it, until its shortcomings have finally
become impossible to ignore, by which point it’s too late. You’ve made your bed,
and now you’ve got to make yourself feel okay about lying in it.
But one really good bit does not a platform make.
The really convenient async runtime is not the only thing you adopted. You also
adopted a very custom toolchain, a build system, a calling convention, a
single GC (whether it works for you or not), the set of included batteries, some
of which you CAN swap out, but the rest of the ecosystem won’t, and most
importantly, you adopted a language that happened by accident.
I will grant you that caring too much about something is grounds for
suspicion. It is no secret that a large part of wha