Myths About Go #
Perhaps, myths is not the best term here. Let’s define it as not-always-applicable-truths.
You can learn Go in a day #
This one has been the source of much frustration among developers who feel they cannot fit to the stereotype. It sugar-coats the whole picture and is, let’s be honest, half-true. Without any doubt, Go is a smaller language than Java, C#, or Rust. It also comes with a syntax that should be familiar to those with C/C++ backgrounds, or have used an Object-Oriented language in the past decade. Indeed, one can skim over Effective Go in a day or so and know enough to start playing around with Go. However, that does not mean that one will become proficient in Go in a single day. The latter will take years, the same way it does with other programming languages.
Learning a programming language’s syntax is perhaps 1% of the whole game. It takes far more time and dedication to figure out when to use the right part of the language to solve the given problem. Especially, for people coming to Go with previous programming experience, expect to have a long period of un-learning certain things. Go is a spartan language, and many of the conveniences one might have been used to, will likely not exist, or will require a lot more boilerplate code than expected. Of course, all of this has a reasonable explanation, but I would like to prepare the reader for a certain period of desperation, once the first few more serious hurdles appear.
One way to solve every problem #
Go is often quoted as having one way to solve a particular problem. Indeed, when compared to more feature-rich languages, there doesn’t seem to be a large variety of constructs to mess with:
- one type of loop.
- one type of map.
- one type of a collection structure.
- everyone formats their code the same way.
- everyone is quasi-enforced to handle errors the same way.
- testing is built right-in
The list can go on. The simpler syntax does lead to code, which in most cases, looks uniform, regardless of whether it was written by one person or a team of 100. However, more is needed to eliminate the room for variety. Having set your footing in Go, the same old questions become apparent here too:
- How do I structure my project?
- Should I use plain functions or create object-like structs and add methods to them?
- What is the best way to use interfaces?
- Should I make every function expect an interface?
- Do I need assertions in my tests?
- Should I call functions with a lot of arguments, or should I rather package those in a struct?
- Where is it OK to return an error, vs. log it, or perhaps, panic?
- Which struct is worth having a constructor function, and which can be left as-is?
- Should I use struct tags? Which ones?
- Is it better to leave struct fields exported, or hide them and create accessor methods instead?
- Is
context.Context
just for cancellation, or can it pass dependencies across?
Like the list above, this one can also go on ad infinitum. Of course, these are problems that are not endemic to Go and will appear in any programming context. I am trying to say that brushing them all by repeating that Go is a “simple language” and there is “one way to do things” won’t solve them for us. The opposite might do us a bigger favor - acknowledge that developing software is a hard problem and that some uncertainty will always be there. This is why an idiomatic approach to programming is ultimately tied to a particular issue, and the people tasked with solving it. It is not a one-size-fits-all thing.
Consistent > Idiomatic #
Idiomatic code refers to code that follows the general conventions and best practices of a particular programming language. It is code that is written in a way that is considered “natural” or “typical” for that language.
As a direct consequence of my previous point, I believe that consistent code is better than idiomatic code. Being consistent does not preclude being idiomatic or vice versa. However, the two may sometimes be at odds due to project specifics which might make keeping a strictly idiomatic codebase a nightmare.
Years ago, when I joined my first big Java team, I had gathered many preconceptions on how Java projects should be built based on best practices outlined by books like Design Patterns and Effective Java. Little did I know, I would soon be thrown into a completely new massive project where many “non-idiomatic” decisions were made to keep the code easy to follow and understand by everyone.
While I objected at the beginning, I quickly realized that those decisions were the best ones for the project’s long-term viability. After a few days, I completely eased into the team’s conventions and could onboard others with the same ease a few months later. Had we stuck with all the “by-the-book” principles, we wouldn’t have been able to scale it to become the market success it is until today.
This does not mean that knowing the principles isn’t important. For the same reason I often page through Design Patterns and Effective Java for some forgotten wisdom, I regard Go’s foundational principles and focus on keeping code idiomatic. And yet, one of the skills that makes a great engineer is knowing when to forgo the principles in the name of pragmatism.
Go Is the Solution for Every Problem #
Saying this hurts me the most because I spent much of the last four years trying to make Go the perfect hammer for every nail. After 20 years of writing code, I should have known better that the perfect hammer does not exist.
To clarify my argument, allow me to split the available software out there into two very broad categories - tooling and business application programs (incl. games and entertainment). Tools are general-purpose and easy to combine with one another to achieve a particular task. Business applications solve high-level problems with thousands of rules and exceptions. They are usually complex and bulky, great at delving deep into a specific problem domain, but not so easy to get to work with one another.
Of course, one might argue that tools are applications or that application programs can be used to help one work with other applications. This objection is perfectly valid - I am simply making a distinction in terms of each category’s focus and scope of application.
That said, I believe that the requirements for making an excellent general-purpose tool differ from those that make a good business application.
Go is perfect for building tooling and general-purpose services. Things like command-line applications (CLIs) or HTTP services that do one particular thing are where the language shines. Go’s minimalism, simplicity, and performance makes it ideal for building such things. This does not exclude the tools from being complex in their own right - Kubernetes is an immensely complex tool, same for Docker and most of the cloud-enabling tools you and I use daily. They all share the same characteristic - they solve a narrow technical problem while staying out of the way, so they can be combined to build even more complex and robust services and applications.
Having used Go to build a few Rails-style business Web applications, however, I concluded that I should have probably used something else (like Rails itself, Spring Boot, or Django). Go can do the job, but after the 50th time of having to implement almost the same business logic, it stops being fun anymore. Code becomes overly repetitive, and the distinction between business logic and low-level code gets washed away, making it hard to follow along. This has been the most challenging part for me when building a business-domain application in Go - seeing the forest for the trees.
There are situations where one needs sheer performance and the ability to build a good general-purpose tool for the job. There are others where the ability to rely on high-level abstractions to focus complex business domain at hand is more important.
Concurrency is Easy #
Go is renowned for its language support for goroutines, which makes concurrency easier at the nuts-and-bolts level. Anyone can fire a goroutine. In fact, anyone can fire up a million goroutines if they want to. Spawning is the easy part. Making those goroutines run in a particular order without stepping onto each other’s toes is a much more challenging problem. Synchronization in Go is just as hard as in different programming languages. That’s why I find the “Go concurrency is easy” tip a disservice - it brings your guard down. One can easily fall into the trap of releasing something that works locally only to see it mysteriously fail in production.