Simplicity

Go is a simplistic language, that is what everyone, despite distantly knowing Go philosophy, would first figure when getting in touch with the language. No matter from what paradigm of programming you come from, Go is a language which you could master within weeks. Coming from a background of Java, C# and C++ myself, I did not really miss the variety of features they provide. On the contrary, I feel much more freedom within the carefully restricted set of things which Go allows me to do.

I’m writing this article on an airplane back from Fosdem 2019, an open source conference held in Brussels, and what I remember the most about the Go talks was one particular slide where the speaker explained the new features added to Go since last year. The slide reads:

“This is intentionally left blank.”

Brilliant! Not only was I delighted because of the humour, but I also appreciate that Go actually sticks to its philosophy of simplicity and no redundancy. “Less is more” after all. Why do we need all the synthetic sugar added just so a couple of lines of code could be saved? For example, Go does not give you the fundamental ternary operator which could easily be substituted by a simple if-else. The following examples are written in C#, only the first is equivalent to Go:

Example 1:

if a != nil {

x = a

} else {

x = b

}

Example 2:

x := a != nil ? a : b

Example 3:

x := a??b

The 1st example is easiest to understand and is applicable in most programming languages.

The 2nd example is more concise but will become hard to read when there are multiple nested values.

The 3rd example is clearly synthetic sugar for one narrow scenario. It’s not apparent what it does if you are not familiar with the syntax. If you ever learn a second language, you’d feel the struggle speaking to native speakers as they don’t always use expressions in text books.

Note simplicity does not equal conciseness. You could write a brilliant one-liner which nobody could comprehend such as the following example of “Conway’s game of life” implementation in APL:

life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}

Or you could write an averagely smart piece of code which everyone in the team could understand and maintain. Software systems have grown too large that hardly anything is built by a single person. As the team grows and members change, the code is not an asset but liability. Brilliant code is code that could be understood by an average developer in the team.

I could rant on about various features, or the lack thereof, but among all, I’d love to discuss the following:

Implicit interface implementation

Missing of inheritance

Circular dependency prohibition

Implicit interface implementation

Before deep-diving into each of the points, I would like to take a step back and enumerate on programming fundamentals and principles. Programming fundamentals are all about abstraction/encapsulation and dependency, in other terms, “who knows what”. Then come programming principles where the fundamentals are elaborated on how we should design abstraction and dependency effectively. Here are some of the principles which I believe are the most significant:

High cohesion/Low coupling

Favour composition over inheritance

SOLID (Single responsibility, Open/Closed, Liskov substitution, Interface segregation, Dependency inversion)

I love this quote when it comes to explaining dependency inversion: “Would you ever solder a lamp directly to the electrical wiring in a wall?” Of course we wouldn’t, that’d be silly right? The modern world relies on specialisation if different areas. There’s no single company which could build an entire airplane. Components are developed all over the world and shipped to a central place for assembling. In order for this to happen, components must be built according to some standards, or interfaces, the same with software systems. Let’s take a look at the following example:

Dependency inversion

Figure 1 demonstrates a strict dependency of object A on object B. It implies that a complete implementation of B must be provided at the time A is constructed, and that A “knows” about B specifically. The problem comes when you want to replace B with C, then you either need to fix B totally or change all references to C. If you are lucky, you won’t need to replace B ever in your life, but another problem comes when you would like to write a component test for object A, then you still need to construct B every time A is tested. If there’s a test failure, you won’t know if the failure is inherent to A or if it’s a bug from implementation of B.

Dependency inversion solves this problem by introducing an interface in the same package where A is located and this interface is implemented by object B. Figure 2 shows us that the dependency now has been inverted as the arrow now comes from B to A. Go decided to take a step further by eliminating all together the dependency between A and B thanks to implicit interface implementation. In languages such as Java and C# you need to declare if a class implements an interface, in a way, creating a strict dependency between an interface and its implementers. Go does not think such relationship should be classified as dependency simply because an implementer does not need its interface to function. Object A does need whatever implementing the interface A to work (dependency), but B simply works without knowing about what contract it conforms to.

Missing of inheritance

This might be one of the boldest design decisions the authors of Go has made, also one of the most brilliant, not supporting the fundamental building block of object-oriented even though Go is a multi-paradigm languague itself. We need to understand that inheritance is a dependency. A subclass needs a parent class to function because the parent class partially dictates how the subclass should behave. In practice, inheritance is also a headache once the level of inheritance spirals out of hand. Take Android’s MultiAutoCompleteTextView for example, it’s the 5th level from android.view.View.

java.lang.Object

↳ android.view.View

↳ android.widget.TextView

↳ android.widget.EditText

↳ android.widget.AutoCompleteTextView

↳ android.widget.MultiAutoCompleteTextView

It’s virtually impossible to get a snapshot of its implementation without jumping back and forth among the inheritance chain with different overriding methods. Complication leads to less readability and maintainability. To further demonstrate the complication of inheritance, let’s take a classic example:

Imagine you are to implement 2 classes “Square” and “Rectangle”, it’s intuitive to let “Square” extends “Rectangle”, right? Not so fast… Check out the code below:

public class Rectangle {

void SetSize(int x, int y) {}

} public class Square extends Rectangle {

void SetSize(int x, int y) {}

}

If Square extends the method SetSize, would it set 2 sides to x or y? This is an outright violation of Liskov substitution as a Rectangle could not be substituted by any Square objects in the system.

Anyhow, the real question is what inheritance tries to solve? Two things: extensibility and substitution. We certain would like to extend a class with more functionality without duplicating code, yet conforming with single responsibility and open/closed principles. Composition and interface come at your disposal. Want to extend a class? Use composition. Want to substitute a class? Use interface.

Circular dependency prohibition

Actually with static linking during compilation, circular dependency is impossible because if A depends on B, B has to be built first. Languages have tried to overcome this “shortcoming” with different approaches such as forward declaration in C++. However, circular dependency signifies something fundamentally wrong in system design and this is easily encountered by inexperienced programmers. Disallowing that forces developers to rethink their design improves code quality. You are free to believe in the interconnectedness of everything in the universe but spaghetti code is not something we are in favour for.

On simplicity topic, Rob Pike, one of the Go authors, had a great talk back in 2015, where he explained how complicated it was to build simplicity into Go, including garbage collection and concurrency. I would strongly recommend it.