Hard truths before switching to Go

Hard truths before switching to Go

Source: https://www.youtube.com/watch?v=UEU4SzBjqrc

Go has a lot going for it. it is developed by some of the most prolific software developers in the world. it constantly ranks high in various developer surveys. and it is a darling of the tech community. on top of that it enjoys some of the best publicity thanks to the large companies and successful projects who are actively using it. just recently, for instance, the TypeScript team announced that they’re porting their compiler and tool set to Go in an attempt to speed up performance and simplify their build pipeline. that’s no small endorsement. when a language created by Microsoft engineers starts leaning on Go to improve its tooling and the resulting performance is magnitudes better than before you know Go’s doing something right. but despite all Go’s success it’s not all sunshines and rainbows. it might sound surprising but while Go is an amazing language it also has its bad parts. and I think everybody who plans to learn or use Go in their next projects should be aware of these caveats. for a bit of context I’m a software developer with more than 15 years of experience and during this time I worked mainly with Java Cotlin and TypeScript. for the past year I decided to transition to Go in all my personal projects for the reasons we all know by now. go is simple fast efficient. it has great tooling. and it is a breeze to compile and deploy. but after actually writing Go for a while I realized things are a lot more nuanced and not everything lives up to the hype. let me explain. this doesn’t mean Go is a bad language. however we need to paint the full picture because just like Go has some amazing parts it also has sharp edges that you only discover once you spend real time building something non-trivial with it. the design decisions that make Go such a powerful tool also have drawbacks. they impact the way you write code especially if you already have a standard and a set of expectations based on working with other languages.

Here are five things you really need to know before switching to Go. it was surprising to find out that based on some Google searches. apparently I’m not the only one noticing these things.

One of Go’s biggest selling points is its simplicity. to be fair it really does look simple on the surface there are few keywords, the syntax is minimal, and the official docs go out of their way to keep the language easy to learn. but once you go beyond the tutorials, you start realizing that Go simplicity is mostly skin deep and it often comes at the cost of expressiveness. it’s the little things which start to nag you after a while. for instance even though Go has a while loop it does not have a while keyword. you just write a for loop and skip the condition. this forced syntax reuse just for the sake of saving a keyword makes reading code more difficult than it needs to be. the same problem extends to public and private modifiers as well. instead of using clear keywords Go uses capitalization to determine visibility. while this approach is concise it’s also easy to overlook especially when refactoring. change the case of a function name and you’ve potentially broken your entire API without any compiler warnings.

and let’s be honest enums are no better. go doesn’t actually have real enums. just a workaround using const and iota. it feels more like a clever hack than a proper language feature. there’s no type safety, no grouping, no scoping and nothing stopping you from assigning any arbitrary value to what’s supposed to be a well-defined enum. it’s like Go wanted to look simple and minimalist at all costs even when this means hiding complexity instead of eliminating it.

Unnecessary verbosity

one of Go’s more distinctive features is the ability to return multiple values from a function. at first, this feels elegant especially when handling errors. no exceptions, no try catch blocks, and no special error types. you just return the value and the error and simply move on. but this elegance fades when you need to do anything sophisticated with these return values. the fundamental issue is that multiple return values in Go aren’t actually tupless or first class values. you can’t store them in a variable and you can’t put them in a slice. you can’t send them through a channel and you can’t abstract over them with generics. this seemingly minor limitation cascades into significant verbosity throughout your codebase. let’s consider the common scenario of processing multiple items in parallel and collecting all results with their potential errors. in languages with proper tupples you’d simply collect the result error pairs. in Go however you’re forced to create a custom struct type just to package these values together. the unnecessary boilerplate code is what made me run away from Java in the first place. i was simply sick of the neverending POJO and the verbosity. this might seem minor if you’re building simple applications. but as your codebase grows the verbosity compounds. you’ll find yourself creating countless small structures just to bundle related values together adding ceremony that distracts from your actual logic.

Explicit error handling

go’s error handling is probably one of the most common complaints whenever developers are first exposed to the language. and let’s be honest managing errors in Go is extremely verbose. the chances are you’ll write the same four lines of code over and over and over again. go calls this explicit error handling and it has its benefits. good software is predictable and safe. so having to deal with errors explicitly forces you to acknowledge when something can go wrong and handle it right away. but there is a big difference between being explicit and being repetitive. the language lacks even the simplest tools to streamline error handling. rob Pike, one of Ghost creators defended this choice. he argued that explicit repetitive error checks improve readability and keep control flow obvious. so in a way it all boils down to the coding style you prefer. but let’s be honest. people are comparing Go’s error handling with solutions offered by Rust for instance and they do have a point. there were efforts to improve the situation. and we even had a proposal to add a built-in try mechanism in an attempt to simplify error handling but it was shut down because the community feared it would ruin Go’s simplicity. in the end this is one of those design decisions which you either love or hate.

go’s inheritance approach faces the same issues since its creators argue strongly against the complexity of languages like Java. in their view inheritance led to fragile tangled code bases. as a result Go’s official philosophy is to avoid inheritance and favor composition. this is a fair point but in running from one problem they might have created another. especially for developers coming from stricter backgrounds. embedding in Go might trick you into thinking it’s inheritance but it is actually a different beast entirely. this approach makes Go code simpler and more predictable in many ways. but it does mean that developers coming from traditional OOP languages need to adjust their thinking. go isn’t trying to be a partial OOP language. it’s offering a different approach to code organization altogether, trading some of the flexibility of inheritance for clarity and simplicity.

go was initially designed without generics and that decision did limit the language for over a decade. generics were finally introduced in 2022 but with a design that maintains Go’s philosophy of simplicity over flexibility. while you can now create generic types and functions there are still limitations. you can define methods on generic types but you can’t add generic methods to non-generic types.

go doesn’t support function or operator overloading and its type constraint system, while powerful enough for many use cases, doesn’t provide the full expressiveness of traits or type classes found in other languages. this aligns with Go’s fundamental philosophy of prioritizing clarity and readability over expressiveness. the language encourages explicit concrete code over complex abstractions. this approach has benefits for maintainability and onboarding new team members. but it can lead to more verbose code in large systems where more sophisticated abstraction mechanisms might reduce duplication.

so at the end of the day it all boils down to expectations and coding philosophy. if you expect Go to match the development experience of a highle language with lots of syntactic sugar you will be disappointed. but if you’re looking for a fast reliable nononsense language that gets out of your way and compiles in milliseconds Go is the right tool for you. just go in with your eyes open because there’s a huge difference between liking a language after watching some YouTube videos and actually still enjoying it after maintaining a real world project with real users, edge cases and deadlines. i expect this one might hit a few nerves so please keep it civil in the comments. if you like this video you should watch one of these ones next.


Links to this note