Developers love turf wars. Tabs vs. spaces, my editor vs. your editor, my favourite language vs. yours. Why one person might like one programming language over another often comes down to syntax choices or quality of life features that the individual programmer prefers. And that's fine, we all have our preferences.
But. Aren't there some more basic requirements we should value higher than superficial language implementation details? For example, consider these questions: Does the language make you write more robust code that fails less often? Is it easy to understand for someone other than the author? Is a feature "good" or "cool" if you can use it but your colleague doesn't understand it or even know about it? After writing code for a living for almost a decade now, these are the questions I find the most interesting.
Go doesn't have many features. In fact, it doesn't have most of what you're probably used to. Things like generics, enums, classes and more is not a thing. This is done for good reasons, of course, but it does make it hard to explain Go to to others, as one typically starts on the back foot.
- Them: "I heard Go doesn't have feature X".
- Me: "Yeah, Go prefers simplicity and you don't need X, it just complicates things".
- Them: "But X is my favourite feature of language Y".
- Me: "Just because you like it doesn't mean its good. If a feature can lead to ambiguity, uncertainty or confusion, it might be bad to use it. And if it might be misused, why have it?".
- Them: "People just need to learn language Y as well as me. If they're not as proficient as me, that's not my fault".
And from there, the discussion more or less falls apart as I try to convince them that their favourite toy might be superfluous and they (understandably) go into defensive mode. So the point is, Go's quirkiness is the first thing people hear about and for many that's also where the discussion or interest ends. Can't use my favourite feature? Forget it then.
There is also the misconception that Go is special purpose language, only suited to certain use cases. I've heard many water cooler conversations saying that Go is only a "micro service language" and can't be used for anything else. I think this stems from the frequent mentions of Go along with Kubernetes or serverless hosting options.
This is of course not true, Go is excellent for many use-cases. Among the most popular are:
- Making web applications. No framework is required, but there are several projects to look into if you want one, such as Revel or Buffalo. The standard library even has (experimental) WebAssembly support.
- APIs, be it REST, gRPC, GraphQL, or WebSockets.
- Console apps
- Micro-services and serverless apps with AWS or Google Cloud Functions.
You can also go exploring this curated list of Awesome Go projects to see the wide range of community projects going on.
As for the claim that Go is best suited for small apps in particular, one of the pain-points Google wanted to address with Go was the difficulty of managing large codebases with existing languages. Many of Go's design decisions was made with this in mind, and to make Go a language that scales well. And if it scales well for Google, it probably scales well for you.
“ We write code for humans, not computers „
But let's backtrack to the more fundamental questions and let me explain what matters most to me in my day to day life as a developer. My "list of demands" for a programming language has changed over the years, from a more technological and programming-skill oriented focus ("I want feature X and Y") in the early years of my career to a more holistic and craftsman oriented focus.
This stems from certain realisations I've had over the years:
- We write code for humans, not computers. We spend a lot more time reading code and trying to understand it, than writing code. The main difference between languages is how convenient it is for humans to reason about.
- Everything breaks, and finding out why and how as fast as possible matters. Understanding exceptions and failures and how they flow through your system is just as important as understanding the happy path.
- Code you write has a long tail and should be written with maintenance and those who comes after you in mind. It often lasts for years, even decades. When you write code, first you must understand it, then your colleagues or team must understand it, but will a newly hired employee in three years understand it? That poor person years down the line is just as important as you.
The things that matter in life
So now that you understand my fundamental world view a little better, let's dive into how this correlates to my requirements for a good day to day development experience:
- I want to deploy code that I trust, and that is stable and reliable in production.
- I want other people to be able to easily read and understand my code.
- I just want to be as productive as possible and only concerned about building my app and not fighting against dependencies, frameworks and the boilerplate code they often enforce.
An ideal programming language for me should therefore support these desires. Let's explain each of the above points in more detail.
To make me trust my code and deploy reliably to production, the language should:
Be predictable in how it crashes and handles errors. This implies two things:
- First, the language should make it as hard as possible for me to F it up. For example, null is a common culprit in crashes so the language should make me make explicit choices in situations where null might occur.
- Second, when the crash or error does occur - and it will - I need to be sure I have a handler for it, and that I have an easy (ish) way of tracing what went wrong. In fact, I want the most explicit handling of an error I can possibly get. If I can read in my code exactly what happens in every case there is a failure, I trust my code a lot more.
As a side-note, ever looked at a try-catch statement and thought "Wait, this Exception thing just appears out of nowhere and lands in this code block? Where did it come from, how did it get here?". If you hadn't thought about this before, I bet you do now, and that it starts feel like magic and maybe kinda iffy.
Be easy to test. This one is straight-forward. The easier it is and the lower the threshold there is to write and run unit tests (not to mention maintaining them over time!), the more tests gets written. It's already hard to convince people to write tests, lowering the threshold as much as possible is important for longevity of a codebase.
Be easy to build and deploy. The less configuration needed, the better. The more configuration forced upon us from frameworks and tools, the harder it is to spread knowledge and deep understanding of those frameworks, tools and the overall workings of a toolchain to all members of a software team. And the quicker people understand how something works, the more they trust it and grow confident using it because there is less uncertainty of what they don't know that they don't know.
Readability and understanding
To make my code easy to read and understood by other people, the language should:
Be as explicit as possible. A file with code in it is more than the IDE-experience, these days almost every team has a pull-request protocol in their workflow so if you want good and proper reviews, the code should be readable and understandable without convention-based frameworks that "just work ™️" and assistive features of the IDE.
Restrict programmers in how many ways there are to solving a certain problem. Every programmer's head is different so therefore their code is too, which is very apparent in most codebases. A good codebase should look uniform, and the more ways of solving a problem are available and the more flexibility a programmer is given, the harder this is to enforce.
To make me as productive and focused on my app as possible, the language should:
Come with batteries included, meaning its standard library should cover all common use-cases for modern applications.
Reduce boilerplate and overhead to get something up and running as much as possible.
Stay out of my way. The only reason my <insert project here> exists is that I need to implement some business rules and features either because I have an idea (personal project) or my boss have an idea (work project). Either way, my goal is to translate that core idea into code as fast as possible, and every detour and yak-shaving along the way only serves to make me annoyed. This is what I mean by being "focused on my app". Everything else is just in the way.
Come with a package manager or built-in tooling for managing third-party dependencies. This is the only tooling-specific request on the entire list, which is telling. Dependency or third-party package management is a quality-of-life feature we've gotten used to now and painless installation and updating of dependencies is important to the overall productivity flow.
Glaringly absent from the list above is any mention of language implementation details such as how it handles memory or garbage collection, how it treats immutability, whether it has pattern matching or not, is it object-oriented or is it functional, what quality of life syntax sugar does it have, and so on.
“ If the language is simple and easy to understand, the code produced by it must be simple and easy to understand as well. „
None of those things makes us, in my opinion, able to write better and more robust code that fails as rarely as possible. This doesn't mean things like quality of life features are completely irrelevant, but it should probably be on the secondary tier in your list of criteria to consider for a language if the goal is to get as many programmers as possible to write as good, robust and easy to understand code as possible.
Simplicity is king
So, to no one's surprise, enter Go.
Go's core philosophy is that if the language is simple and easy to understand, the code produced by it must be simple and easy to understand as well.
The result is a language that has a pragmatic approach to its design and very few features, and that's tailored for making blazingly fast programs with modern tooling in a straight-forward manner.
This does mean that several things you take for granted in other languages doesn't exist in Go, and at first you might take offense at this. Over time, however, you start to see the reasoning behind the decisions and the brilliance of it. And before you know it, you look at your old projects and notice how much boilerplate there really is and how much of a mess there can be when languages just add new features without restraints.
A sidenote on generics
Perhaps the most heated debate in the Go community is whether the language should implement generics or not. The Go authors has refused generics since the beginning, and the lack of it has turned many curious developers away.
The proposal for generics in Go was accepted as recently as February 2021 and the goal for the team is to hopefully have an early version included in Go 1.18 later in 2021.
There is of course a raging discussion over whether this is a good thing for Go or not. The complexity of generics as a concept is a concern for many, as it lets developers write less explicit code and make more generic and abstract libraries, which in some people's eyes only makes code less readable and less simple. It also makes the compiler and the language itself more complex.
On the other side, the flexibility generics offers is of course exactly what many developers want, downsides or not.
Personally, I'm sceptical but hopeful. To be honest, I haven't dug too deep on the topic yet, as I wanted to see where the pieces landed first. I see the dangers of the complexity it might allow for, but I'm also hopeful that it'll reduce a lot of "boring code" we have to write now due to the lack of it. I also trust in the Go authors to make smart decisions and be good caretakers of the language.
If you're curious about generics in Go, I've gathered some resources at the very bottom.
Starting the Go journey
After that, dive deeper with this article Go at Google: Language Design in the Service of Software Engineering and this article Go: Ten years and climbing.
Go is a quirky language to get into. It does things in weird ways, but for good - if not obvious - reasons. In my opinion though, the positives outweigh the negatives. I particularly love the way it handles errors and how "to the point" my code gets.
At the end of the day though, it's like any other language and framework: Just another tool we might add to our toolbox, to be used in the right situations.
As we all (should) know by now, there are no silver bullets, no one solution that is perfect for every occasion and that has no downsides. It's up to you - to us - to learn about the tools available to us, and choose the right tool for the job.