As I will be switching clients next year, I will also be switching programming languages, from Go back to Java. Truth to be told, I’m relieved because of it. The biggest challenges faced were most definitely not related to the choice of programming languages, but after cursing and fighting with Go’s mechanics and philosophy for years, I’m ready to throw in the towel. Depending on the type of software you’re developing, that is. Sometimes, Go should be a No-Go.
It is undoubtedly true that I curse and fight with any language or technical toolkit that doesn’t immediately do what I want. Yet most of those times, that’s because I don’t know better. As in the case of Go’s biggest personal irks, it’s because of how Google’s team envisioned Go (won’t fix). I’ll probably get slammed for this but I don’t care as this is my blog and my own way of tracking thoughts in public. So here’s a list of things that make me sad when programming in Go.
Go is boring and that’s not good.
Russ Cox, one of the original tech leads, is often quoted for saying that Go is boring and that’s good. There is only one way to loop (for), there is no way to filter, map, or reduce without looping yourself, and there are no other fancy tricks embedded in most other languages. Good, right?
Here’s what Reddit user rochakgupta says better than I can summarize it:
I wonder where people draw the line between boring and powerful. Go has its qualities, but please do not for a second think that it is being the “hot new flashy thing” by leaving out the bare essentials which are slowly gonna creep into the language as time goes, anyway.
Guess what, the sambler/lo package (maps, filters, contains, find, …—you know, the essentials that are missing) has 18.1k stars and is used by 12.6k other GitHub projects. The first thing we usually do is go get that thing. Since then, as rochakgupta correctly guessed, some of these funcs crept into the slices package, but of course not enough to be functionally interesting.
To me, Go is boring, and that’s not good. I don’t want to write loops again and again, I want to map, and then filter. Yes I can extract that into a separate receiver method, but guess what:
Go discourages Clean Code principles.
Get out your pitchforks folks, here it comes. If I want to extract a func, chances are 99% that the second return type is going to be error, meaning yet another if err != nil will have to be inserted. While trying to clean up clutter, you introduce clutter. Error handling in Go drives me crazy. At one point I was considering relying on panic() and the panic recovery middleware instead of having to put up with this crap. Because of these mechanics, my HTTP handlers in a very small project are 20 lines or more when they should be 4. I just can’t extract or clean up this mess.
Go discourages long names for variables, methods, and funcs. What’s a c or an a? Command? Controller? Argument? Amendment? Okay I’m exaggerating here, receiver shorthands are not likely to cause that much of a stir, and worst case, you can use cmd instead. But the point is that because of these mantras, Go fanatics overly focus on making things as small as possible, causing endless discussions in our team when for instance code reviewing test method names.
Go is purposely small, you should (never) Do It Yourself.
Want a simple HTTP handler? Batteries included, right? Nope, that line is rightfully reserved for Python only. It’s indeed very simple to bootstrap a server or client, but as soon as you require default middleware (exponential backoff, cross-site whatever, etc etc), you have to scrape together multiple packages and hope (1) they’re still actively maintained and (2) they work as you expected them to.
Again, the design philosophy to keep things simple almost seems to mandate developers to keep on reinventing the wheel. I bet you wrote a filter yourself because depending on another one just for that is a no-no, am I right?
Go’s package ecosystem does not seem very mature. We often stumble upon GitHub projects that are abandoned or only have released two versions. Of course, it’s not fair to compare a relative young programming language to the vast and stable available packages of say .NET/Java. And yet, that’s exactly what I’m doing, sorry. Go’s biggest ORM package, Gorm, is light-years behind Hibernate/Entity Framework in terms of functionality, and full of undocumented weird behaviour. What do Go nerds say? You don’t need ORM in Go! Do It Yourself!
In Go, there’s only one way to do things.
While I usually applaud conventions and a uniform way to do things—if it reduces technical debt and increases readability—this part of Go’s boringness is just untrue. Want to write a test? Sure, how about a table test? No? Okay, what about a test suite using stretchr/testify (also used by 557k other projects because obviously that should not be a part of the core language)? No? Okay, what about a custom subtest in a table test as part of a test suite? You see where I’m going with this?
This shouldn’t be a problem as it’s just a matter of having good agreements within the team. But precisely because of these conflicting philosophical statements of the core Go team (who are not consistent themselves), we encountered more instead of less instances of never-ending discussions. Go is opinionated, I get that. They refuse to put in assertions and the explanation sounds a lot like fuck you, you’re a bad programmer. The result is go getting Yet Another Package.
Debugging in Go is not fun.
If you have ever tried evaluating expressions during debugging sessions, you’ll know what I mean. Custom string representations of objects at runtime are also out of the question. Stacktraces and/or logs of failing unit tests are very confusing if you run thousands of them in a CI. Even if that last argument is a configuration error on our part, it feels like I’m debugging C instead of Go. Which makes sense as it can rely on the same (debugging) toolchain.
I’d rather “go” for a developer debugging experience instead of an optimized binary, thank you very much. I presume Rust learned from Go’s mistakes by providing very clear and good error information when something goes wrong—most of the errors come with suggestions that are dead-on.
Go’s built-in package, test, and performance monitor toolchain are all great, but as with all things in life, these come with a cost: the cost of having to deal with boring and plumbing code. It’s up to you how to interpret the word boring. I haven’t even touched “import cycle not allowed” in tests or trying to do DDD: overly relying on the strange struct embed mechanism is just asking for misery.
I think Go is perfectly fine for infrastructure work—after all, Docker, Drone, Hugo, and the like are written in Go. But please don’t try to write an ERP packet in Go, even if your CTO is a Google fanboy and rolled out their Super Duper Google Stack Migration Plan.