I agree with almost everything about this article. I am also a known Go zealot. What gives?

Well, I think the author underestimates just how much needs differ across different programs. Go excels with services because they are often written with a shared-nothing mindset for horizontal scalability. If you’re using S3, Redis, PostgreSQL and RabbitMQ for all of your persistence and coordination, it leaves only small bits for your service to be concerned about w.r.t. local data races, and they are mostly encapsulated into libraries, sometimes provided by the language, that can be aided with static checking.

There is something in video games called minmaxing, which is where you maximize one attribute at the expense of all of the others. For me C++, and now Rust, feel like they are guilty of this with their weight on zero-cost abstractions. I am not against efficient code. It is super awesome to generate efficient JSON serialization code at compile time. In aggregate, even a few hundred cycles per request could be a big difference.

But as many have already stated, the secret to zero cost abstractions is that it’s zero runtime cost. Just as reducing programming language complexity doesn’t remove it, but only moves it around, removing runtime cost doesn’t remove it either. And the problem here isn’t that compile time isn’t a better time than runtime; it’s the hidden costs that suck. Sure, maybe the compile times in Rust will improve, and maybe it’s not even that big of a deal. But every little feature the language has puts some additional stress on the ecosystem. The language server has to bear this load, for example.

Everyone knows the downsides in Go’s approaches to code generation, but I actually think it’s kind of intriguing. go:generate is imperfect in almost every way, and yet it’s oddly practical. It doesn’t guarantee code is not stale, but clever codegen design can cause syntax errors when regenerating code is needed. But maybe most interestingly, gopls doesn’t need to care. The go compiler doesn’t need to care. The parser doesn’t need to care. As far as they are all concerned, the generated code is just more code. To me, this is clearly an approach that doesn’t suffer from minmaxing. Maybe it suffers from a dogmatic fetishized idea of simplicity, but I don’t think so: I think it was literally just meant to be the bare minimum to try to solve the most important parts of the problem, and at that it succeeds.

I am not saying that Rust isn’t awesome. In fact, what I am saying is that I think a lot of smart people have fallen into the trap of thinking that Rust is the one true programming language simply because it’s such an alluring story. But, I think there’s more room. Formally verified C, Go, Rust and Zig will probably all co-exist for the foreseeable future, along with many other permutations of programming languages. Nobody is going to write a web browser in Go, or at least not anything to the scale of Servo or Chromium. But, if you wanted a language to write some servers in, well, hell, you could certainly do a lot worse than Go.

I don’t want to go too into it, but I also think there is some overly optimistic viewpoints here. It is true that reducing language complexity will often move it elsewhere, but that does not mean that language features do not cause complexity that doesn’t exist otherwise. For example, the Rust borrow checker. Yes, I love it. It is not perfect. It allows only a subset of valid, correct code, and it’s excessively hard to write, for example, a correct doubly linked list in Rust. Does that mean it’s useless? No. But, in general, it’s not very hard to write a correct linked list. (It’s easier to write a subtly flawed one, but that’s neither here nor there. :) Thankfully, most people do not need to write a linked list implementation, but I do still think this stands as an interesting illustration nonetheless.

And I am not suggesting that it’s a superior option at all, but if you are in a simple language like Go, it’s still possible to eat some cake and have it, too. gvisor’s checklock program is a pretty good example. Not perfect in almost any regard, but again, it represents a different set of tradeoffs.

Rust frontloads an awful lot of complexity for cases that are not necessarily common for everyone. And that complexity, both in the toolchain and the resulting code, is a cost that gets paid repeatedly forever. It needs to have a good payoff. Systems programming and game developers have a lot to gain. Everyone else? I think it’s up for debate.

May the best languages prosper. Ideally cooperatively.