Golang Bad: Why Its Design Falls Short

Author

Reads 1.3K

Hands on a Laptop Keyboard
Credit: pexels.com, Hands on a Laptop Keyboard

Golang's design has some notable flaws, making it less ideal for certain use cases.

The lack of a built-in type system is a significant drawback, as it can lead to type-related errors and inconsistencies, as seen in the "Golang's Type System: A Major Design Flaw" section.

Golang's reliance on interfaces can make it difficult to reason about code, as it often leads to indirect references and unclear dependencies, as discussed in "Golang's Interface-Based Design: A Recipe for Disaster".

This can result in brittle code that's hard to maintain and debug, as experienced in the "Golang's Fragile Code Base" section.

A unique perspective: Golang Design

Go's Design Limitations

Go's ecosystem is surprisingly sparse when it comes to data structures, with only a handful of options beyond the built-in slice and map.

The reason for this scarcity is that library-defined data structures in Go are a pain to use, requiring casts and sacrificing type safety. This is because there are two categories of data structures in Go: the built-in slice, map, array, and channel, which are type safe and generic, and the rest, which can't provide type safety and are clumsy to use.

Readers also liked: Golang Copy Slice

Credit: youtube.com, HARD truths before switching to Go...

The built-in structures are so convenient to use that they make library-defined data structures seem like a bad deal. The Go ecosystem's lack of data structures is a direct result of this trade-off.

To illustrate this, let's look at the example of using a concurrent map with sync.Map, which is a great illustration of the limitations of Go's data structures.

The duality between built-in structures and Go code is painful in more subtle ways, too. For example, when we want to write reusable algorithms, we have to define new types that implement multiple methods just to bridge a generic algorithm and the typed slice.

Here are the two categories of data structures in Go:

  • aristocracy: the built-in slice, map, array, and channel
  • rest of the world: library-defined data structures

This duality is the root of many problems in Go development, including the need to repeat boilerplate code for each and every type we want to sort.

Nil and Uninitialized Values

Nil and Uninitialized Values can be a real pain in Go. You might end up with a monstrosity like this.

Go has two types of nil: nil itself and zero-initialized values. This can be confusing, especially when dealing with structs.

In Go, if you declare a struct without initializing its fields, they will be zero-initialized, which is not the same as nil. This can lead to unexpected behavior.

Intriguing read: Golang Go

The Var Dilemma

Adult male programmer working on code at a modern desk setup with a large monitor.
Credit: pexels.com, Adult male programmer working on code at a modern desk setup with a large monitor.

Go provides two ways to declare a variable and assign it a value: var x = "foo" and x := "foo".

The main differences are that var allows declaration without initialization (and you then have to declare the type), like in var x string, whereas := requires assignment and allows a mix of existing and new variables.

With var, you have to declare the type, which can be a hassle, but it also helps prevent accidental type mismatches.

The := syntax is more concise, but it also makes it easy to accidentally shadow a variable, as I was caught more than once by this.

This can happen when you use := (declare and assign) which is too close to = (assign), as shown in the example.

In Go, the := syntax is especially useful for error handling, as it allows you to declare and assign a variable in one step, like in the example with err.

However, this syntax also forces the error variable scope to be wrong, making it harder to read and understand the code.

This can slow down an experienced coder, who will see these things and wonder why the variable is in scope for the rest of the function.

Expand your knowledge: Golang vs Go

Nil Types

Credit: youtube.com, GopherCon 2016: Understanding nil - Francesc Campoy

Nil Types are a source of frustration in programming, often resulting in errors like the monstrosity mentioned earlier.

There are two types of nil, which can be a challenge to work with.

You may end up with multiple nil values in your code, making it harder to track down issues.

One common issue is dealing with optional values that can be nil, leading to errors if not properly handled.

Memory Use

RAM is cheap, but paying for it by the cloud instance or container is not.

You can manually trigger a GC run with runtime.GC(), but the experts warn against it.

90% of the time, a trust-in-the-compiler approach works, but then it doesn't.

I rewrote some Go code in another language because the Go version used more and more memory over time.

The cost of RAM adds up quickly, especially when running containers on the same machine.

Giving a thousand containers 4TiB of RAM instead of 1TiB is a significant expense.

Mutability and Concurrency

Credit: youtube.com, Golang Tutorial #18 - Mutable & Immutable Data Types

Mutability is a major concern in Go, and it's not just about structs being mutable. In fact, there's no way to define immutable structures in Go, and the const keyword doesn't apply to struct fields.

This means that even if you pass a struct by value, it's not necessarily immutable, especially if it contains pointers or references to mutable data. The built-in collections like maps, slices, and arrays are references, and copying a struct that contains one of these just copies the pointer to the same underlying memory.

You have to be extremely careful about this, and not assume immutability just because you're passing a parameter by value. It's not just a theoretical concern either - it can lead to some nasty bugs and race conditions.

Go's concurrency model, which relies on channels, is supposed to make concurrent programming safer, but it doesn't prevent race conditions on shared data. In fact, once you send a pointer on a channel, it's game over - you're sharing mutable data between concurrent processes.

And it gets worse - even if you send a struct on a channel, it's not necessarily safe, because slices and maps are intrinsically mutable, and any mutation method defined by an interface is an open door to race conditions.

Additional reading: T Golang

Slice and Array Issues

Credit: youtube.com, Golang Arrays & Slices (Interview Question)

Re-slicing a slice in Go doesn't copy the underlying array, it just creates a new view that follows the mutations of the original slice.

This means that sub-slices of a slice are just views and can be affected by changes to the original slice. To avoid this, you need to use the copy() function to separate a slice from its origin.

Appending values to a slice can also cause problems, as it resizes the underlying array if it doesn't have enough capacity. This can lead to non-deterministic bugs that are hard to find.

The effects of appending values to a sub-slice can vary depending on the capacity of the original slice, making it harder to debug.

Here are some key takeaways to keep in mind:

  • Re-slicing a slice doesn't copy the underlying array.
  • Sub-slices are just views that follow the mutations of the original slice.
  • Appending values to a slice can resize the underlying array.
  • The effects of appending values to a sub-slice can vary depending on the capacity of the original slice.

These issues can cause problems when working with slices and arrays in Go, and it's essential to understand how they work to avoid common pitfalls.

Struct and Field Issues

Credit: youtube.com, Resolving undefined Issues in Go Struct Printing: The Case of NYTimesNews

Go's struct field tags are a raw string that can be used to store metadata, but they're not formally defined and can lead to awkwardness when using multiple libraries.

The language spec says these tags are ignored at compile time, but can be parsed at runtime using reflection, which can panic if the syntax isn't right.

This can lead to tedious tagging, especially when working with JSON, where public fields must use UpperCamelCase, but the common convention for naming fields in JSON is lowerCamelCase or snake_case.

The standard JSON encoder/decoder in Go doesn't allow providing a naming strategy to automate the conversion, which is why fields in the Docker APIs are all UpperCamelCase.

The JSON parser is case insensitive, which makes it easier to read camelCase JSON, but also means that writing the same data structure back will use the default UpperCamelCase naming and produce a different JSON.

Go's lack of macros to generate boilerplate code is a pain point, especially when dealing with struct field tags and JSON naming conventions.

Here's an interesting read: Convert a Map to Json Golang

Repository and Navigation

Credit: youtube.com, Transaction Management and Repository Pattern | Ilia Sergunin | Conf42 Golang 2023

Repository and Navigation can be a challenge in golang. Go has weaknesses, and navigating repository files is one of them.

The language design of go can make it difficult to determine if a problem is due to a design flaw or user error. This list can help you quickly answer the question.

A good starting point is to understand that go's design is not inherently flawed. It's just that go has its own way of doing things, which can be unfamiliar to new users. Go's design is based on simplicity and efficiency, but this can sometimes lead to unexpected behavior.

To improve your navigation skills, it's essential to know how to use go's repository files effectively. Go's repository files are organized in a specific way, which can be confusing at first. But with practice, you can become proficient in navigating them.

Go's repository files are not just a collection of code; they are a well-structured system that helps you find what you need quickly. By understanding how to use go's repository files, you can save time and reduce frustration when working on a project. Go's repository files are designed to be easy to use, but they still require some knowledge to navigate effectively.

Intriguing read: Golang Use .env File

Language and Design

Credit: youtube.com, Rob Pike: What Golang Got Right & Wrong

Go's syntax is often criticized for being too simple, which can lead to a lack of expressiveness in the code.

This simplicity can make it difficult to convey complex ideas or nuances in the language.

The use of a single return statement can lead to code that is hard to read and understand, as seen in the example where a single return statement is used to handle multiple error cases.

This can result in code that is more prone to errors and harder to maintain.

Take a look at this: Golang Source

No Generics for You

Go, a statically typed language, surprisingly lacks generics, making it hard to conceive a modern language without this feature.

This means you can't define reusable abstractions that work with any type in a type-safe way, forcing you to use untyped interface{} and cast values to the proper type.

Any mistake will only be caught at runtime, resulting in a panic.

This is similar to the pre-Java 5 era, a time before type-safe generics were introduced.

Go's segregation between built-in generics and user-defined non-generics has far-reaching consequences, impacting not just developer comfort and compile-time type safety, but the entire Go ecosystem.

Advances in Modern Language Design

Credit: youtube.com, 50 Years of Programming and Language Design

Go's creators ignored advances in modern language design, and it shows. They chose to use a Go assembly directly inspired from Plan9, which could have been replaced with LLVM for a wider range of target architectures.

The use of a Plan9-inspired assembly language is puzzling, especially considering the existence of LLVM. It seems that if you need to write assembly to get the most out of the CPU, using the target CPU assembly language directly would be more straightforward.

Go's design appears to be disconnected from the rest of the programming language world. Its creators seem to have worked in a parallel universe, oblivious to the developments in compilers and programming language design in the 90's and 2000's.

A fresh viewpoint: Golang Use Cases

It Had to Be This Way

Go's design happened in a parallel universe, where most of what happened in compilers and programming language design in the 90's and 2000's never happened.

The creators of Go ignored advances in modern language design, opting for a Go assembly directly inspired from Plan9 instead of using LLVM, which would have provided a wide range of target architectures out of the box.

Credit: youtube.com, Computer Science - Brian Kernighan on successful language design

Go's goal was to replace C and C++, but it missed its target, as C and C++ developers at Google didn't adopt it, likely due to the garbage collector, which low-level C developers fiercely reject.

The killer app for Go was Docker, which triggered its wide adoption in the devops world, attracting users of scripting languages like Python and Ruby.

Go doesn't have constructors, insisting that the "zero value" should be readily usable, a simplification that brings benefits mostly for the language implementors.

Interface methods in Go don't support default implementations, which is why the new Unwrap method for errors is a "convention" rather than being part of the error interface.

Interface and Type Issues

Go's interface system has some quirks that can lead to issues. One problem is that interfaces are structural types, which means a type doesn't need to explicitly specify that it implements an interface. This can make it hard to find what types implement a given interface, as it relies on function definition matching.

Expand your knowledge: Check Type of Interface Golang

Credit: youtube.com, Creating an interface for dig in Golang: Common Errors and Solutions

Here are some examples of the drawbacks of Go's structural typing:

  • Types may unknowingly implement an interface because they have the corresponding methods, but the semantics of the implementation may be different from what is expected from the interface contract.
  • Adding a method to an interface can break existing code, as it requires updating all types that implement the interface.

Go's lack of generics also causes issues, particularly when it comes to reusable algorithms. While Go doesn't need generics, it's still a limitation that can lead to cumbersome syntax and runtime errors.

Nil Interface Values

Nil interface values can be tricky to work with, especially when they're not what they seem. An interface value can hold a nil concrete value, and yet still be non-nil itself.

This is because interface values are fat pointers, which means they contain two elements: the pointer to the method dispatch table and the address of the actual object. In the case of a nil interface value, the second element is the address of the actual object, which is nil.

The call to a method that acts on a value, like Boom, will dereference the pointer, causing a panic. On the other hand, a call to a method that applies to pointers, like Bang, won't dereference the pointer, so it will succeed.

Credit: youtube.com, Understanding nil Interface Values in Golang

To safely check a nil interface value, you need to nil-check both the interface value and the value pointed to by the interface object, which can be done using reflection.

This behavior can cause subtle bugs and is considered a flaw in the language design. It's not just a matter of writing a test in a safe way; it's also a matter of understanding how interface values work in Go.

Related reading: Golang Check Type

Interfaces Are Types

Go interfaces are like structural types, they define behavior that is later implemented by a type. This means a type doesn't need to explicitly specify that it implements an interface, it just has to implement all functions defined in the interface.

Structural typing has several drawbacks, including making it hard to find what types implement a given interface. This can be frustrating, especially when you're used to searching for classes that implement an interface in languages like Java or Scala.

Credit: youtube.com, Interfaces and error handling

A type may unknowingly implement an interface because it has the corresponding methods, but the semantics of the implementation may be different from what is expected from the interface contract. This can lead to unexpected behavior.

In Go, interfaces are types, and this means they have the same rules as other types. For example, all methods related to a type must be defined in the type's package. This can make it difficult to add new methods to an interface without breaking existing code.

Go recommends having tiny interfaces with very few methods to prevent this issue. However, this can also lead to a proliferation of small interfaces, which can be cumbersome to manage.

Here are some of the drawbacks of structural typing:

  • Hard to find what types implement a given interface
  • Adding a method to an interface can break existing code
  • Types may unknowingly implement an interface

Dependency and Management

Dependency management in Go is a nightmare, with no official solution in place. It's a major pain point for developers.

Jaana Dogan, a well-known gopher at Google, has vented her frustration on Twitter about the issue, stating that she's considering quitting Go if it's not resolved within a year.

Credit: youtube.com, Golang UK Conference 2015 - William Kennedy - Dependency Management

There is no dependency management in Go, with all current solutions being just hacks and workarounds. This is a result of Google's original approach to building everything from a single monolithic repository.

Adding a dependency in Go means cloning the dependency's source code repo in your GOPATH, which means you get the current master branch at the time of cloning. This can lead to issues with different projects needing different versions of a dependency.

You can't even have your own project live in a separate directory without hacking per-project GOPATH or fiddling with symbolic links. This is a major limitation of the current system.

The community has developed workarounds with a large number of tools, including package management tools that use vendoring and lock files to provide reproducible builds. However, these solutions are not ideal and don't provide proper version management.

The vendor directory was officially supported in Go 1.6, but it's still about vendoring what you cloned, not proper version management. This means you're still stuck with conflicting imports from transitive dependencies.

Things are getting better, though, with the introduction of dep, the official dependency management tool, which supports versions and follows semantic versioning conventions. However, it still requires your project to live in GOPATH, which is a major limitation.

Take a look at this: Golang Version Manager

Frequently Asked Questions

Is Golang being phased out?

No, Golang is not being phased out; it's gaining importance in scalable cloud applications, blockchain, and performance engineering. Its relevance is expected to continue growing in these areas.

Claire Beier

Senior Writer

Claire Beier is a seasoned writer with a passion for creating informative and engaging content. With a keen eye for detail and a talent for simplifying complex concepts, Claire has established herself as a go-to expert in the field of web development. Her articles on HTML elements have been widely praised for their clarity and accessibility.

Love What You Read? Stay Updated!

Join our community for insights, tips, and more.