Back

avivcarmi.com

We Need To Talk About The Bad Sides of Go

10/17/2022Updated 3/14/2026
https://avivcarmi.com/we-need-to-talk-about-the-bad-sides-of-go/

This is a story about the downsides of the Go programming language, the part about it that makes us less productive and our codebases less safe and less maintainable. ... ### Lack of Visibility Modifiers The first root cause of namespace pollution is the lack of visibility modifiers. In what I believe to be an effort to reduce redundant keywords and enhance simplicity, the language designers decided to omit visibility modifier keywords (`public`, `private`, etc…) in favor of symbol naming. Symbols starting with an uppercase letter are automatically public and the rest are private. Sounds like a great choice to promote simplicity. But over time it’s becoming clear that this method has a stronger downside than upside: In most other languages, type names, by convention, begin with an uppercase letter, and variable names begin with a lowercase one. This convention has a very powerful implication — it means that variables can never shadow types. … ``` |1 2 3 4 5 6 7 8 9 10|// type shadowing in Go type user struct { name string } func main() { user := &user{name: "John"} anotherUser := &user{name: "Jane"} // compilation error: user is not a type }| |--|--| ``` This is very common in Go, and I bet most Gophers run into this at some point. In most cases, your coding scopes deal with one user instance, so naming it `user` should be a clear and reasonable choice. However, in Go, whenever you store a private type into a private variable or a public type into a public variable — you run into this. **So you simply start naming your user variables** `u`. … Other problems with lack of native enum support? What about **iterating all possible values** of an enum? That’s something you need every now and then, maybe if you need to send it to the UI for a user to choose one. Not possible. What about **namespacing**? Isn’t it better for all HTTP status codes to reside in a single namespace providing only HTTP status codes? Instead, Go has them mixed with the rest of the public symbols of an HTTP package — like Client, or Server, and error instances. … Initially, I was furious with the designers of the library for not enforcing prevention of such a mistake, “if it happened to me” and everything. Then I thought about it more and realized, they can’t. Go simply does not provide it. At glance, struct literals look like the perfect candidate for config params use case, allowing you to pass in exactly what you need, and omit the rest. But turns out it’s the opposite. … In Go, wrapping each error with another error up the entire call stack, and then garbage collecting all the created objects is almost as expensive. Not to mention the manual coding. If this is what you’re advocating, you’d better off in a try-catch environment, to begin with. Stacked traces can be very useful when investigating errors and bugs, **but the rest of the time they’re highly expensive redundant information**. Maybe a perfect error-handling mechanism would have a switch to turn them on and off when needed. 🤷‍♀️ … This is terrible in terms of performance. But more scary is the fact that it’s not guaranteed. Obviously, `"oops...network error"` can change at any time, and there isn’t any compiler enforcement to help us out. When the author of the package decides to change the message to `"oops...there has been a network error"`, **my error handling logic breaks**, you’ve gotta be kidding me. … In my opinion, the lack of solid community conventions in combination with an ineffective async return value mechanism is the **root cause of terrible coding**, and this is kind of a **standard in the Go community.** [my deepest and sincere apologies to everyone who’s hurt by this paragraph]. Some examples? How about a 400-line function in one of the most popular HTTP frameworks in Go 😨, how about a 100-line function in Google’s gRPC library? … ## Summary If you combine community conventions and naming problems with async return value problems, you end up with **hugely popular libraries** shipping code with complex, 100+ line functions, using one-letter undocumented variables, declared at the other side of the package. This is extremely **unreadable and unmaintainable, and surprisingly common**. In addition, as opposed to other modern languages, Go doesn’t provide any kind of runtime value safety. This results in many **value-related runtime issues** which can easily be avoided.

Related Pain Points6

Error handling patterns are verbose and outdated

7

Go's repetitive if err != nil pattern is seen as verbose boilerplate compared to modern error handling in Rust and TypeScript. Developers report fatigue and decreased productivity in large codebases, and 28% of survey respondents want language features for better error handling.

dxGo

Go lacks modern language features like generics, enums, and pattern matching

7

28% of developers want language features missing from Go that are available in other languages. Common requests include proper enums, union types, sum types, pattern matching, and nil pointer safety. Existing generics are criticized as half-baked.

compatibilityGo

Struct literals don't prevent required field omission, causing silent bugs

6

Go struct literals appear suitable for config parameters but provide no compiler enforcement for required fields. Omitting mandatory fields compiles without error, leading to silent bugs at runtime.

languageGo

Error string matching breaks when library maintainers change messages

6

Error handling depends on matching error message strings, which are not compiler-enforced. When package authors change error messages, downstream error handling logic breaks silently with no compile-time protection.

compatibilityGo

Symbol-based visibility causes variable shadowing and naming conflicts

5

Go's reliance on symbol case for visibility (uppercase=public, lowercase=private) creates unintended variable shadowing. Developers cannot use natural names like 'user' for a user variable because it shadows the user type, forcing awkward one-letter naming conventions.

languageGo

Expensive error wrapping and stack trace overhead

5

Error wrapping up the call stack and garbage collection of intermediate objects is nearly as expensive as try-catch, yet requires manual coding. Stack traces provide redundant information most of the time and lack a mechanism to toggle them.

performanceGo