Back

bravenewgeek.com

Go Is Unapologetically Flawed, Here's Why We Use It

10/15/2016Updated 3/23/2026
https://bravenewgeek.com/go-is-unapologetically-flawed-heres-why-we-use-it/

We use Go *because it’s boring*. Previously, we worked almost exclusively with Python, and after a certain point, it becomes a *nightmare*. You can bend Python to your will. You can hack it, you can monkey patch it, and you can write remarkably expressive, terse code. It’s also remarkably difficult to maintain and slow. I think this is characteristic of statically and dynamically typed languages in general. Dynamic typing allows you to quickly build and iterate but lacks the static-analysis tooling needed for larger codebases and performance characteristics required for more real-time systems. In my mind, the curve tends to look something like this: … ### The Untype System To put it mildly, Go’s type system is impaired. It does not lend itself to writing quality, maintainable code at a large scale, which seems to be in stark contrast to the language’s ambitions. The type system is noble in theory, but in practice it falls apart rather quickly. Without generics, programmers are forced to either copy and paste code for each type, rely on code generation which is often clumsy and laborious, or subvert the type system altogether through reflection. … The argument there, I suppose, is to rely on interfaces to specify the behavior needed in a function. In passing, this sounds reasonable, but again, it quickly falls apart for even the most trivial situations. Further, you can’t add methods to types from a different (or standard library) package. Instead, you must effectively alias or wrap the type with a new type, resulting in more boilerplate and code that generally takes longer to grok. You start to realize that Go isn’t actually all that great at what it sets out to accomplish in terms of fostering maintainable, large-scale codebases—boilerplate and code duplication abound. It’s 2015, why in the world are we still writing code like this: Now repeat for uint32, uint64, int32, etc. In any other modern programming language, this would get you laughed out of a code review. In Go, no one seems to bat an eye, and the alternatives aren’t much better. … Another idiosyncrasy is adding an item to a channel which is closed. Instead of returning an error, or a boolean, or whatever, *it panics*. Perhaps because it’s considered a programmer error? I’m not sure. Either way, these behaviors seem inconsistent to me. I often find myself asking what the “idiomatic” approach would be when designing an API. Go could really use proper algebraic data types. … In actuality, to write high-performance Go, you end up throwing away many of the language’s niceties. Defers add overhead, interface indirection is expensive (granted, this is not unique to Go), and channels are, generally speaking, on the slowish side. For being one of Go’s hallmarks, channels are a bit disappointing. As I already mentioned, the behavior of panicking on puts to a closed channel is problematic. What about cases where we have producers blocked on a put to a channel and another goroutine calls close on it? They panic. Other annoyances include not being able to peek into the channel or get more than one item from it, common operations on most blocking queues. … But upon closer inspection, we realize this approach is subtly broken. While it works, if we stop iterating, the loop adding items to the channel will block—the goroutine is leaked. Instead, we must push the onus onto the user to signal the iteration is finished. It’s far less elegant and prone to leaks if not used correctly—so much for channels and goroutines. … ### Dependency Management in Practice For being a language geared towards Google-sized projects, Go’s approach to managing dependencies is effectively nonexistent. For small projects with little-to-no dependencies, *go get* works great. But Go is a server language, and we typically have many dependencies which must be pinned to different versions. Go’s package structure and *go get* do not support this. Reproducible builds and dependency management continue to be a source of frustration for folks trying to build real software with it. … A few other technical points: – “Cannot add methods to types from a different package”: Would you want something like extension methods in C#? – “Channels are slow”: I’ve read there is some work going on to improve them. That said, you admit yourself that Go is still one the best option among the “concurrent” languages. Are you aware of another language with a similar mechanism builtin in the language or the library and that performs better on this matter?

Related Pain Points6

Dependency management and go get don't support version pinning at scale

7

Go's go get and package structure don't support pinning dependencies to different versions, making reproducible builds and dependency management frustrating for projects with many dependencies. This is a critical gap for a language geared toward large-scale projects.

dependencyGo

Channel panic behavior and missing operations create footguns

7

Sending to a closed channel panics instead of returning an error or boolean. Channels also lack common blocking queue operations like peeking or fetching multiple items. Producers blocked on a closed channel panic, and improper usage easily leaks goroutines.

languageGo

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

Channels perform poorly and lack performance optimizations

6

Despite being a hallmark of Go, channels are slow and throw away language niceties like defers and interface indirection when optimizing for performance. Developers must abandon many of Go's features to achieve high performance.

performanceGo

Debugging complexity in large and dynamic codebases

5

Python's dynamic nature makes debugging difficult and time-consuming, especially in large codebases. Cryptic error messages and the need to trace through dynamically-typed code makes it hard to identify root causes of bugs without strong debugging tools.

testingPython

Cannot extend types from other packages, requires wrapping boilerplate

5

Go doesn't allow adding methods to types from different or standard library packages. Instead, developers must create wrapper types, resulting in additional boilerplate and code that is harder to understand and maintain.

languageGo