GoLang — The Good, the Bad and the Ugly

Supun Setunga
Geek Culture
Published in
9 min readMar 10, 2021
Image by Garry Tailor via https://images.app.goo.gl/zAysEkk5BBWebt9g6

This article discusses the good, the bad, and the ugly sides of Go programming language.

The Good

Simple

Go is simple. That has been one of the prime design goals of Go as well. It also makes it easy to learn and adopt the language very quickly. Some of the areas that Go is much more simple and light-weighted are:

  • A minimal set of language constructs.
  • Simple project and module structures.
  • Minimalistic visibility control (only public and package-private).
  • Easy to define types (structs).
  • Easy to write tests — no need for external testing frameworks.

Fast

Go programs being compiled down to machine code, and having a static type system, makes it really fast during the execution. Also, the startup time is much less than to something like Java or any JVM language.

Built-in Build and Package Management

One area Go has done really well compared to traditional/old-school programming languages like C++, Java, etc. is that it comes with a built-in build system and a package management system. This eliminates the requirement for a third-party package and build management systems like Gradle, Maven, make, etc., and makes life so much easier for developers.

Type switch

I have been using a lot of Java, and a bit of C++ over the years and the type-switch is something that I always missed in those languages. It no longer needs to do expensive operations such as instanceof checks in Java with a whole lot of if-else conditions or introduce alternative switching properties to objects/classes (like a tag) to switch. Go’s type-switch does the trick. And not to mention, it is insanely fast!

Goroutines

One of the most powerful features of Go is how easily you can write concurrent programs using the language’s native constructs. Read here for a good article on different concurrency patterns available in Go.

Values vs Pointers

Go supports both pass-by-value as well as pass-by-reference. This is really powerful as you don’t get restrained by the language on what you want to do (like in Java).

However, this can also leave some confusion. See the next section.

The Bad

Values vs Pointers — Confusion

  1. Nil checking for pointers is confusing

Check the below example. When you look at the NewApple() method, it returns a nil. However, if you run this main method it will say, “It's not nil”!

type Fruit interface {
}
type Apple struct {
}

func NewApple() *Apple {
return nil // return nil
}

func Main() {
var fruit Fruit = NewApple()

if fruit == nil {
fmt.Println("It's nil")
} else {
fmt.Println("It's not nil")
}
}

The reason is, the NewApple() returns a pointer to a nil, and the pointer itself is not nil. To check whether the value pointed by the pointer is nil, you will have to check fruit == (*Apple)(nil). But again, the downside is you need to know that this returns an Apple.

2. Assigning values will make a ‘copy’

Assigning a ‘value’ to another variable will copy the value unless specified. See the below example:

apple := Apple{Price: 50}newApple := applenewApple.Price++fmt.Println(apple)

The value will still print as {50} , because it has assigned a copy into newApple variable, and the increment happens on the copy. Also, values being copied every-time an assignment happens means it will have a big impact on performance-critical applications.

But Go also has a solution, which is to pass the reference, instead of the value:

apple := Apple{Price: 50}newApple := &apple    // pass the pointernewApple.Price++fmt.Println(apple)

However, the catch is, if you are not being very careful with this pointer vs value difference, not only the program will start to behave in unintended ways, but also the application will be extremely slow.

3. Maps and Slices are passed by reference by default.

Well, we just talked about the difference between a ‘value’ and a ‘pointer’. But surprise! maps and slices don't have a pass-by-value concept. They are always passed by reference. This is inconsistent and different from all the other structured types, which could easily confuse users.

No constructors for structs

Go structs doesn’t have a constructor concept, primarily because go is not designed as an OOP language. But unfortunately, that would mean there is no way to ensure proper initialization of structs fields. It is possible to introduce a constructor-alike function that would do the job. But yet again, the problem is, due to Go’s minimalistic visibility control capabilities, there is no way to make ensure someone will only use that function, instead of the struct initializer expression.

Not much Libraries

Whenever you start to write some serious programs with Go, you will often find yourself looking for libraries that are not actually there. Go’s strategy has been to keep the built-in libraries minimal, and to rely on the community to build the echo system. Unfortunately, it hasn’t really gone the way it has planned, and even to use some simple ‘collections’ data structures, you will have to write down something on your own.

No IDEs

This is a bit of a surprise for a programing language that has been there over a decade to not have much ‘free’ IDE support. There are a few IDEs that support Go, but they have certain limitations.

  • IntelliJ IDEA Go plugin — Only supports enterprise edition. Does not support the community (free) edition.
  • Eclipse Go plugin — Deprecated and no active development happens.
  • GoLand — Has good features. But again, no free version.
  • VSCode Go plugin — It's free, but has fewer features compared to GoLand. Finding interfaces/implementation for a struct/interface is not supported in the VSCode plugin. This is the deal-breaker since Go has a structural type system, and figuring out these manually is quite impossible without the IDE support.
  • Atom plugin /VIM plugin — Not really fully-fledged IDEs.

The Ugly

No Inheritance — Writing OOP is a Real Pain

This is the biggest pain point I’ve come across when I started working with Go. It is true that Go is not an OOP language by design, but in reality, when you are working with large code-bases, it is almost impossible to avoid objects and modularization.

Here's a simple example: I was writing a simple parser, which produced a simple syntax tree that consists of nodes. Each node has a ‘tag’ or a ‘nodeKind’ which is used to uniquely identify the node (in aces like serialization and deserialization). Then there's a getter method, say getTag() which returns the tag of each node.

So the implementation of nodes look like this:

type Node interface {
getTag() int
}
type FunctionDefinition struct {
tag int
}
type VariableDefinition struct {
tag int
}
type BinaryExpression struct {
tag int
}
...
// Implementing the getTag() method for all nodesfunc (node *FunctionDefinition) getTag() int {
return node.tag
}
func (node *VariableDefinition) getTag() int {
return node.tag
}
func (node *BinaryExpression) getTag() int {
return node.tag
}
...

Straight away you can see the problem here. The getType() method has to be implemented for each and every node, despite all the methods looks identical and does exactly the same thing. Now, imagine this syntax tree having close to 50 nodes (a fully implemented syntax tree for a modern-day language can have that many nodes)— you’ll end up writing and duplicating the same code fifty times! To make it worse, if there are more such methods, that means the work gets increased in the factor of 50 (yikes!).

The problem here is the lack of inheritance in Go. In true OOP programming languages like Java and C++, this can be much more easily achieved by extending the ‘Node’ class that has the implementation of getNode(), rather than implementing it for all the nodes.

Note: Go has the ‘Composition’ feature, which is the same as having a field of a struct type T, but with methods of T being available. This shouldn’t be confused as ‘inheritance’, though it may help to get around with some pain points.

No Explicit Interface Conformance

Take the same example as above. Here, all the nodes comply with the Node interface only by implementing its methods. Other than that, we have never mentioned anywhere that FunctionDefinition implements Node. In other words, interface implementation is implicit. Again, this is how Go’s interfaces are designed to work with structural typing. This makes it harder to figure out which struct implements which interface.

Imagine you are introducing a new function to an existing interface. Now, none of the existing nodes who were implementing the interface are no longer conform to that interface. But, how do I know that they are broken now, and more importantly, which of the structs out of all the struct in my codebase are broken? Simply, there's no way to figure it out.

In a large code-base, updating the implementations after changing the interface can be a nightmare. It's pretty easy to miss out and you may never know it until your code breaks in the production.

A Hack:

There is a hack you can do to make sure a node actually implements the Node interface, by doing:

var _ Node = FunctionDefinition{}

This is simply trying to assign a value of FunctionDefinition to a variable of type Node, which would give a compile-time error if you change the interface. However, this is an ugly hack and you’ll end up writing the same code fifty times for the fifty nodes (again).

No Generics!

As I mentioned earlier, Go doesn’t have a very extensive third-party library base. And as a result, Go doesn’t have some basic data structures like ordered-map, sets, etc. I came across a need for an ordered-map to hold different values at different times. (for example, a map of function definitions, and a map of variable definitions, etc.). Since there is no built-in/third-party library for ordered-maps, I had to write one by myself.

But, Alas! there is no way for me to write a generic map that I can re-use for all of my different use cases because Go doesn’t have generics. This means I have to either:

  • Duplicate the implementation for different value types,
struct FuncDefMap {
// implementation
}
func (m *FuncDefMap) set(key string, value FunctionDefinition) {
// implementation
}
func (m *FuncDefMap) get(key string) FunctionDefinition {
// implementation
}
struct VarDefMap {
// implementation
}
func (m *VarDefMap) set(key string, value VariableDefinition) {
// implementation
}
func (m *VarDefMap) get(key string) VariableDefinition {
// implementation
}
  • Or, write an ordered-map with interface{} type as the value type, and then implement wrappers that cast/assert the value to the respective type.
struct AnyOrderedMap {
// implementation
}
func (m *AnyOrderedMap) set(key string, value interface{}) {
// implementation
}
func (m *AnyOrderedMap) get(key string) interface{} {
// implementation
}
// Implement type-safe wrappersstruct FuncDefMap {
AnyOrderedMap
}
func (m *FuncDefMap) set(key string, value *FunctionDefinition) {
m.AnyOrderedMap.set(key, value)
}
func (m *FuncDefMap) get(key string) *FunctionDefinition {
return m.AnyOrderedMap.get(key).(*FunctionDefinition)
}

But both of the above methods require defining new structs for each map type we need.

Error Checking — Every Damn Time!

Well, probably this is the most famous developer-experience blunder with Go. Panicking and recovering is not the ‘Go’ way of handling errors. Any error that can-be/should-be handled, must be returned as an error from the enclosing function. What that means is, sometimes you’ll end up writing more error handling lines of code than the actual business logic.

Note: There a proposal in Go2, for solving this problem by introducing a ‘check-expression’. But it is still unknown when will Go2 be released.

The Verdict

Go is fast — from starting-up to running long-term applications. Has a good built-in build and package management system. It is good for applications that use functional programming heavily. Also Go provides simple yet powerful concurrent programming capabilities with goroutines and channels.

However, it can miserably fail when working with a large project with a large code-base, especially if object-oriented capabilities are heavily required. The maintainability of the code also can become a problem as the code-base grow.

Thus, it is wise to think through what you want to do and what you will be doing in your project, before choosing Go as your primary programming language. If you would be writing a lot of object-oriented programs, then Go maybe not the best one to go with. But, if you are writing some light-weight apps, such as microservices that need quick start-up times and runs fast with a low memory footprint, then Go would probably full fill your needs.

--

--

Supun Setunga
Geek Culture

Compiler Developer | Statistician | Machine Learning Enthusiast