This is a story about the good and the bad sides of the Go programming language, about it being a time for a change, and about how carefully such a change must be handled. This is the first part of a 3-article series. In this piece, we’re going to set the scene and discuss why, for many use cases, Go can be the best choice. 💡⭐
In This Series
- What Makes Go the Best Language
- We Need To Talk About The Bad Sides of Go
- A Proposition For a Better Future
The Go programming language is exploding with growth these last several years. Big companies, as well as small start-ups warmly adopt it, it’s getting as close as it can to being a standard language for cloud-native systems. It’s also been the most desired language to learn for several years in a row (HackerRank study, 2020) and even the fastest growing language (according to OSS study over GitHub repositories, 2022).
Us Gophers, we absolutely love this language…but we also constantly wish to change it (🤔🤷🏽♂️?). There are so many open GitHub issues containing language change proposals, so many online discussions, and the grand finale in the shape of the recent introduction of generics, the biggest language change up until now. A change that divided the community in two — those who warmly embrace it, and those who believe this will forever change the language.
The Go community and its maintainers tend to be very consistent regarding language proposals. As they should. It goes hand in hand with the language’s most basic principle, and as a Gopher, you must agree with it at some level — it’s all about simplicity and readability.
The Breaking Point
Generally speaking, we all agree that simplicity should be highly valued. Code should be easier to read than it is to write. How can you argue with that logic? Every professional developer knows that you write a line of code once and then maintain it for 5 years. In a very permissive language, you may be tempted to write that line of code in a very fancy way, and then everyone else is scratching their head trying to figure it out. Including your six months older self.
You could say we all agree that simplicity is important. However, simplicity comes at a scale, and at some point, you start trading off productivity. 📈📉 This is where Gophers differ. Some Gophers, maybe even most of us, believe that the language today is where it should be. Perhaps even prior to the arrival of generics. Any further addition will hurt readability, productivity, and maintainability. 😵💫
Other Gophers believe the language should support new features. There’s a huge list of language proposals for features aiming to enhance productivity and reduce ceremony and code duplication. Most of them get rejected for simplicity’s sake. In a recent episode of the Go Time podcast, Kris Brandow referred to this exact breaking point saying “I think it’s time for Go to have a fork”, this will allow keeping the language as it was originally designed while allowing an extended flavor of the language to evolve. 💡
If such an event is to take place, I believe it’s a critical moment for the Go community. I believe designers of such a new dialect must be responsible enough to adopt some proposals and features but retain Go’s core values. This will determine whether Go and such a fork can seamlessly interact, and can share the same runtime environment, libraries, and ecosystem.
This kind of responsible decision-making is important to ensure we remain a single community with a single ecosystem, believing in the same core philosophy, while offering an alternative flavor, allowing sacrificing some simplicity to gain maintainability and runtime safety. 🛡️👷
There are well-known, hugely successful precedents for such a move. Unarguably, the JVM ecosystem will last longer and keep on gaining popularity thanks to Scala and Kotlin (a decrease in Java’s popularity is overtaken by an increase in Scala’s, during the previous decade, and in Kotlin’s, during this one — source). All three languages contribute to a stronger, single community and gain stronger libraries and integrations. JavaScript has undoubtedly become stronger thanks to Typescript, which quickly became one of the world’s most popular languages itself. 😱
I also believe this is the right move for us Gophers. But why do I think that? And why do I think Go is a perfect candidate for such a secondary dialect? We’ll slowly work our way to a full answer in this and the following article, discussing the good and bad sides of the language.
What Am I Fighting For?
So the first question that pops to mind is — if you don’t like Go, why aren’t you switching to a different programming language? A one that supports more features and more expressive syntax. I’ll answer this question from a personal point of view, but I believe this answer also captures the sentiment of many other Gophers.
Simplicity
One of the most basic ideas of Go is — there shouldn’t be more than one way to do something. Personally, I strongly agree with this one. I’ve wasted too many hours discussing the coding choices of functions written in JavaScript, Python, or other feature-rich languages. Such discussions were never about the integrity of the code, nor about performance. More often than not, those discussions were all about optics and coding flavor. To me, this is a regrettable waste of precious engineering time. But more importantly, it dramatically hurts readability. Being used to writing code in a certain way makes reading it in other ways unnecessarily harder. 😱 I’d rather sacrifice support for shiny, fancy tricks and shortcuts, and gain reliability and reliability. 💪
This is very fundamental to Go’s philosophy, and I believe it to be one of the most powerful ideas in coding in general.
On the subject of simplicity, a few words about one-liner coding culture. Languages like Python and JavaScript offer rich (even syntactic) support for functional programming that developers often tend to abuse. There are many online examples (like this one and this one). In the first, you’ll see this one-liner:
1
for i in input().split():print((str(bin(int(i[0],16))[2:].zfill(4))+str(bin(int(i[1],16))[2:].zfill(4))).replace("0"," ").replace("1","X"))
And a claim that writing fewer lines of code should generally be a goal. I’m not into shaming any particular developer, instead, I’d like to argue that this is the result of the coding culture of the community. Obviously, this is not necessarily mainstream to any language, but it is widely used. I believe we can all agree that this kind of coding can very quickly become less readable, less maintainable, harder to debug, and sometimes even hurt performance. Go’s readability-over-writability culture encourages the opposite, and you’ll very rarely stumble across such styling in Go codebases, nor Gophers advocating stuffing as much logic into as few lines possible.
I’m well aware that this particular example and many others are just a fun exercise, but I also know that these habits tend to make their way into real codebases. I’ve sinned myself, and I’ve seen many others do too. Sometimes it’s just fun, other times it seems “elegant”, or just too tempting to avoid.
Concurrency Model
Us Gophers, we love throwing ‘native concurrency’ to the air, but Go didn’t invent anything other than designating a language keyword for spawning new threads. Most other languages support spawning new threads with one line of code and no external dependencies. Is Go really more native in that regard? Also, We like to mumble about goroutines and channels, but those too, are not that special. Channels are simply blocking queues, and goroutines are just a spin-off of coroutines, a concept that’s been around since the 50s. 😱
So what is special about Go’s concurrency model? Quick answer — of all popular languages, Go has the most consistent concurrency model. In fact, it’s the only major language with a fully consistent concurrency model. Remember what we said before about not having more than one way of expressing something? Well, each of the following languages has at least two ways of expressing and managing concurrency: C, C++, C#, Rust, Python, PHP, Ruby, Java, Kotlin, and Scala. They all have both blocking and async APIs.
In most cases, if you go for the blocking API you will not be able to fully utilize your resources. This means you’re either too slow or too expensive. Or both.
To avoid that, modern applications tend to use async APIs, but a ton of problems and caveats immediately join the mix. First of all, it’s not always very clear whether a function or a library is, in fact, async. The compiler can’t help you there so you have to rely on documentation. If the doc doesn’t make it clear, you find yourself digging into the source code. 🥴🔫
Usually, it’s a huge problem if you mistakenly call a blocking function in an async environment. An extreme example is Vertx. Vertx is an async Java framework implementing a single-thread event loop similar to JavaScript’s. This is an architecture in which a single thread handles all the application tasks one after the other. Those tasks trigger non-blocking operations and quickly free the thread to move on to the next task in the queue. If this thread is to hang, the entire application stops responding. Imagine having a production instance running Vertx, handling thousands of requests per second, suddenly freezing for several seconds due to an accidental call to a blocking function on this single thread. This can create a ripple effect that can be deadly for production environments. Vertx designers even went so far as monitoring the event loop thread trying to detect function calls taking too long to return. I haven’t dived into implementation but I assume, unless turned off, it actually consumes production resources to do so. Can you code with confidence and actually focus on business value while constantly considering this lurking danger? Each time you import a new library, each time you call a new function? Scary stuff…🤡😨🤡
Let’s move on to JavaScript. Oh, the beloved JavaScript. It provides a more consistent runtime environment for concurrency. You simply can’t perform blocking calls. How’s that for confidence? In your face, other languages! 😎😎 But wait, what is that? Syntax and readability issues storming our way 😨 Oh no, readability is now terrible. Not even terrible, it’s hell. So much so that the JavaScript community even refers to it as the ‘callback hell’. Much like in other languages, writing and reading async code can be very tough. In JavaScript, it was so intolerable, that the community added native support for promises to the language spec (promises and futures, originating back in the 70s). Promises add some flexibility for handling async results, but also improve readability. Still, it’s next to impossible to follow the execution flow of complex async flows, properly reason about them, or identify issues. So JavaScript went another step further and added async/await support (originally introduced in C# in the early 2010s). This one is truly awesome. ✔️✔️ It gives you the readability of a blocking code and the performance of an async one. However, We’ve ended up with three syntactical models to handle concurrency — callbacks, promises, and async/await. The latter will probably grow up to be a de-facto standard, but legacy code and even new code written by rusty developers will always remain a potentially harmful inconsistency. 🪳 An annoying reminder of different days. 😑 It’s not that uncommon, too. For instance, NodeJS native SDK provides only a callback-based API. If you write a modern application you either have to use a 3rd party to adapt it to your concurrency model, or do it yourself. Most libraries provide several concurrency models, often within a unified API, which makes function signatures unintuitive and harder to use correctly.
Another piece of pleasure that comes along with JavaScript is its single-thread architecture. Since everything in JavaScript is non-blocking, we can, theoretically, work with
a single thread and remain performant, while eliminating the complexity of multi-threaded environments like race conditions and deadlocks. Sounds good, isn’t it?💡 That’s a beautiful theory, no doubt, and the source of appeal that drew many developers, myself included, towards JavaScript about a decade ago. The problem, however, is that in practical terms, blocking I/O is just one way of blocking threads, alongside any CPU-consuming operation. In other words, although JavaScript does not allow blocking I/O, any CPU-intense operation you’re going to perform can potentially demolish your application performance. 💣🧨 Now you have to be careful of long-running loops, hashing algorithms, regex calculations, anything that could, someday, run for too long. Otherwise, you’re in trouble. Again, that scary stuff from before. 🤡😨🤡 How long can you go, providing business value without really consuming CPU? Suffice that you perform a bit of JSON parsing, or use a Redis cluster (whose client calculates hashes on each call), those are very basic examples of operations that could go CPU-intense rather quickly. And when they do, my heart goes for your infrastructure engineers. 💔
Okay. Enough about other languages, let’s talk about Go. Its runtime, to me, is by far the most powerful and reliant for building scalable and concurrent applications. As I said, Go is the only language in all the languages mentioned above whose concurrency model is fully consistent. How so? Well, it only provides a blocking API. Or should I say it only provides an async API? Because the syntax is always blocking. You never need to suspend, await or pass a callback. You call a function and it returns a value. This is as clear and readable as if no I/O operations are involved at all. However, under the hood…everything is completely async. 🚀 Go runtime achieves that by implementing coroutines, or as they’re called in Go — goroutines. In most languages, spawning a new thread results in an allocation of a new OS thread. OS threads are not cheap. In addition to consuming resources, context switching between them is highly expensive. When you use those to perform blocking operations you actually consume a lot of resources to sit down and do nothing while waiting for an I/O response. Go runtime does it differently. It lets you spin up as many goroutines as you like, internally they’re completely independent, just like regular threads, but externally, Go allocates just the amount of OS threads it actually needs, and with those, it only performs async system calls. Spin up thousands of threads in other languages and you’ll suffocate most machines. Spin up millions of goroutines and you should be fine. Try it out, it’s a very simple test to perform.
Powerful stuff huh? The code is simple, the performance is optimized. 💪 But hang on, there’s more. Go is not the first language to use coroutines or any kind of virtual or lightweight threading. However, Go is the only one of the above-mentioned languages that had it right from the get-go, and that never switched. The results in a single, fully consistent model — no matter which library you use, what framework you’re comfortable with, they all share the same model, and the most powerful one. 🤯💥 Even if Go is to add support for promises at some point — it will merely be a new way to track the execution of running threads, it will not require changing the shape, signature, or the behavior of called functions in any way. This is simplicity. This is confidence. This is powerful.
Oh, and it’s also very fast compared to other garbage-collected languages.
Community and Tooling
Lastly, Go is not shy of any other language in regards to community and tooling. This is a very careful statement, a lot of Gophers, including this one☝️ believe that some aspects of those are superior compared to the competition. Built-in support for profiling, code formatting, data race detection tools and benchmarking tools, shipped alongside the compiler and a package manager with no centralized registry are all examples of such. I deliberately avoid glorifying those because there are pros and cons in each tool and most languages have their own neat set of tools and rich communities. However, Go makes a strong competition in that regard, to say the least.
What Else?
Gophers also like to mention that Go is easy to learn, it’s scalable, it has an active community, it’s fast, highly demanded, garbage collected, it has fast build times, it produces small, single-file binaries, and it can run in many different environments. I strongly agree with most of these points, and there are many articles out there that will reveal more interesting features of Go. I wanted to focus on the special powers that make Go unique and superior to other languages.
Summary
In this article, we’ve discussed the state of the Go community and the calls for change and presented the stronger sides of Go — what makes it a special language and what makes its runtime a uniquely powerful environment for writing and running scalable and reliable applications.
In the following article, we’re going to discuss the weak sides of the language — the main focus areas that make Gophers demand a change.