
GoLang was designed extremely well
So well that it makes me enjoy writing imperative OOP code in it as much as I enjoy writing pure functional code in Haskell.
For the last 4 years of my life, I have really dedicated myself to mathematics. I was planning to become a hardcore logician but at the last moment, I decided not to pursue a professional math career, because of my vision of how math should really be done. And instead to learn how to design and implement large-scale distributed systems in a good way and seek professional realization as a Software engineer.
This is my first blog post here at Medium and I will list all the features of Go that have been very well thought out and designed by the language’s creators.
For those who have not heard about GoLang, I will say to definitely check it out. It is a statically typed multi-paradigm garbage collected language that compiles to machine code and it was created for writing systems like the ones made inside Google. The reason behind it is very simple: solve the problems which C++ and Java have and be a language that is designed to run on modern hardware and be productive and very pragmatic.
I believe that Go’s take on the OOP style is actually the right take on what we consider OOP style these days (not what Alan Kay has in his head when he first presented the term). I will talk about this later but it is one of the many things why I really really love Go so much and enjoy writing code in it. Actually, I believe Go’s take on many concepts of other languages is the right one for the purpose the language was build for. This is because the original creators of Go and the people who joined them after the language was open-sourced are very smart and pragmatic people with awful a lot of experience and knowledge. And they have decided not to repeat the proven mistakes of the mainstream (popular) languages.
Before I begin listing all of the features which makes me love Go so much I would like to publicly thank the creators of the language: Rob Pike, Robert Grisember, and Ken Thompson, and the people who later joined them: Andrew Gerrand, Russ Cox, Brad Fitzpatrick, and Alan A. A. Donovan for making Go the language it is. Also to the many people who helped spread the word about Go or/and contributed to the Go source code. You are awesome!
Semicolons in Go are written only in the places they are really needed. Most of the time the programmer does not write them but they are added by the lexer.
The condition of an if statement in Go is not surrounded by parentheses and the opening curly brace is required and put on the same line as the if keyword. if statements in Go also support optional initialization statement which is used to set up local variable(s). Here is an example if statement that checks if it is safe to do type assertion.
There is only one type of loop in Go and this is the for loop. This might seem crazy at first glance but it is actually one of the many things that are very well thought out.
for loops in Go just like the if statements do not need surrounding parentheses which boosts productivity.
for loops in Go have optional initialization and update statements and if they are not present the loop is equivalent to while loop from the most imperative languages. And if there is no condition this is a shorthand for an infinite loop. Yes, a while loop in Go is spelled for loop :)
In practice, only the do-while loop from most languages does not have a direct equivalent. But it can be easily simulated and here is an example.
switch statements in Go are very powerful and designed in the right way for an imperative language. Just like if statements and for loops a switch statement does not need parentheses over the expression being switched-over. By default, cases do not fall through which eliminates the need for repeated and annoying breaks. A list like checking is built-in. Multiple values can be present in a comma-separated list after a case keyword to mean: “in any of the following cases”. Also, switches are not limited to integer values or constant expressions only. Here are examples of switch over strings and a custom type.
There is no pattern matching in Go. In the last three months in which I mostly wrote code in Go, I can say that I have not missed this dream functionality even once.
If a switch statement does not include an expression that is switched over by default it is assumed that the expression being switched over is the expression true. This may seem like a strange decision made by the creators, but it’s actually extremely well thought out because it allows us to write code that kinda feels like pattern matching. Also, this design decision allows the refactoring of large or complex if-else chains in a very clean and explicit way. Here is a quick example to demonstrate that.
There is one more form of the switch and this is the so-called type switch. Which I believe is one of the coolest and powerful features of Go. As the name suggests in Go we can do switches over types as well :) And here is a quick example.
We can also bind the result of val.(type) to a local variable which will ensure type-safe access to data and methods specific to a particular type in each case and will also eliminate the need to manually do a type assertion in each case.
The last example I chose is not a random one. I wanted to list all primitive types in Go with the exception of byte and rune which are type aliases for unit8 and int32.
As it can be seen Go has a built-in type for strings. I will discuss it later, but here I will say that it is designed perfectly!
Also, it is easy to see that Go has built-in support for complex numbers which if you ask me can be a handy feature of the language.
One more thing to notice are the types float32 and float64 which are way better names than the usual float and double. The same goes for int32 and int64. And here is a question to explain why. What is short and what is long long?
Go has one unusual but very useful keyword - defer. This keyword is used only inside functions or methods and allows us to execute function or method calls after the function or method normal flow has ended either by returning or by panicking.
In this blog post, I will not talk about the panic in Go which is also an exotic one since I don’t use it very often and I don’t find it that much useful as the other features of the language. But it has its purpose and it is the language for a reason.
When deferring a function or method call, this call get’s pushed on a stack and this is why the deferred calls are executed in the reverse order of their order in the source code. This again is not a coincidence but it’s on purpose! deferred statements in Go most of the time are used for clean up or resource releasing. At the beginning of this blog post, I said that Go has very well thought support for the OOP style, but it does not have concepts like constructors or destructors. Again this is on purpose and actually, the reason is very very simple: there is absolutely no need for them! The language has all that is needed: functions (and methods) and nothing that would make things overly complicated.
When a function call is a deferred one. The function and its arguments are evaluated in the current scope and then this call gets pushed to a stack. For a method call, the only difference is that the method receiver is also evaluated in the current scope. The evaluation in the current scope is again on purpose and it’s for avoiding bugs or undefined/unexpected behavior and it’s actually what the developer would expect. Here is a quick example of a function that is supposed to be safe for concurrent use.
I said that deferred statements are used for a clean-up and resource releasing so here is a quick example with files that also shows that Go has first-class support for functions.
Go supports returning multiple values from a function or method and this is used heavily. The only requirement is that when more than one value is returned parentheses must wrap the return types. Here is a quick example with a demonstrative purpose.
At this point maybe there will be someone who wonders why I have not used the so-called ternary operator from other languages? Well here is the answer: in Go, there is no ternary conditional operator. I would lie if I say that I have not missed it even once. In fact, at least once a week I get into a situation where I want to say: damn this could have been a single line of code and not 3 or 5… But I totally agree with the designers of the language and I respect their decision. Here is the answer from the FAQ page of the Go website. Yes, the truth is that the ternary operator is overused and in a lot of cases leads to hard to read code…
In Go, there are no implicit type conversions. Every type conversion must be explicitly stated by the developer. To put it simply in Go there are no operations that mix different types. Again this is a brilliant decision made by the creators. Definitely implicit type conversions in languages like C and C++ have caused not one or two bugs. For example, being able to put boolean expressions in numeric expressions turned out not to be a good idea.
In Go, constants are untyped by default but separated into groups. There are 6 groups: boolean, string, integer, float, complex, and the special nil. The fact that constants are untyped plays very well with one very cool feature of the language which is the new types type declarations. But I will not discuss it here.
Constants in Go are evaluated at compile-time so they are restricted compared to most languages. Even more, restricted than constant expressions in C++ versions after C++11. In Go, constants can be created only from constant literals or expressions built from constants. This means no function call can be present in an expression used to define constants.
Constants groups are divided into 4 hierarchies: boolean, string, number, and nil. This means that only the number hierarchy consists of more than one group. The hierarchy is what everyone would expect: integer < float < complex. The number hierarchy allows us to mix literals of different numeric groups in one expression, but the final result is always from the maximal group which has a representative.
Each group except nil has a default type for the untyped constants of that group. For booleans the type is bool, for strings the type is string, for integers the default type is int, for floats the type is float64, and for complex numbers the default type is complex128.
Those default types enable the short variables declare-and-initialize construct which is definitely one of my favorite features of Go. This feature may not seem like a killer feature but try writing a lot of Go code for 2 months and then try to wring code for an hour or two in C, C++, Java, or C# and you will see what I mean.
Go semantics are entirely Pass-by-value which makes things simple and forces us to be explicit which in my opinion is a really great and smart decision. To achieve Pass-by-reference like semantics in Go we use pointers or a built-in reference type: slice, map, and channel or function.
At the beginning of this post, I said that Go is garbage collected but right above I said that in Go we use pointers. At this point, if you are someone new to Go there is a high chance that you will tell yourself: wait what a garbage collected language that uses pointers, WTF? Well, this is just another great decision that the creators made. The truth is that every popular programming language today uses pointers one way or another. The only difference is that most of them hide this from you believing that they make our lifes easier this way.
While Go depends heavily on pointers it avoids all of their pitfalls from languages like C and C++. As I said in the beginning Go’s creators wanted not to repeat any proven mistake made by other popular languages. So they decided that Go will not support pointer arithmetic for the typed pointers! I will repeat myself by saying again that the original designers of Go are very smart and pragmatic people with awful a lot of experience and knowledge and they came with a brilliant idea for the design and implementation of dynamic arrays and strings in Go and I will talk about them in the end since they are one of the best features of Go!
Because Go is garbage collected we can return pointers to local variables with no problem or worries!
Functions are first-class values in Go. Go support anonymous functions (I don’t like the term lambda functions. In my opinion, it’s just a really bad name even when used in the context of Lambda calculus) with full support for lexical closures. Here are two classic examples.
Here is the truth about type declarations (annotations) in Go: They are remarkably well thought out. Their design is just crazy good! Once again I want to thank the creators for making Go, you Rock! If you want to know why just read this great blog post from Rob Pike.
Seeing the first example maybe someone would wonder why I have not generalized the comp function. Well at the time of writing this post Go still does not officially support Generics. There is A Proposal for Adding Generics to Go. And I believe that by the end of 2021 we will have them :) Definitely support for Generics is the feature I really miss the most in Go, but I totally understand why Go lacked Generics for more than 10 years: they are very hard to do right and there are a lot of complications that probably most people have not even thought about. Just go and watch all the talks Bjarne Stroustrup did about Templates and Concepts in C++.
Well after I said that functions are first-class values in Go the next logical thing to say is that in Go errors are just values and of course I believe that this is just brilliant. Because it enables great error handling. The full language is at our disposal. By convention, if a function or method returns an error this is the last returned value when multiple values are returned at once.
Rust also has a nice take on error handling in the form of Result which is a BiFunctor, but Rust also has Generics…
I personally believe that the keywords throw, catch, and finally (exceptions) are bigger mistakes of Java than Null Pointer References: The Billion Dollar Mistake.
I already said that Go in my opinion took the right take on the OOP style. But Go does not have classes it has only types! Custom types can be separated into three different categories: new types, structs, and interfaces. In this section, I’ll talk about structs.
Java has classes it does not have structs and this turned out not to be a great decision… Because yesterday Oracle officially announced The Arrival of Java 16. And guess what? Records are finally an official language feature of Java.
Well, Go did the reverse. In Go, we have structs that could introduce behavior or logic through methods and that’s really it. The developer is the one who decides if he will use a given struct just for data or will add behavior or logic to it. And yes, I believe that Bjarne Stroustrup made not so good decision when he added the class keyword to C++. He could have gone only with a struct and there wouldn’t be any real difference. Having only structs allows code to grow and be extended very naturally and easily. It also helps with refactoring or introducing changes which in other languages would definitely be breaking.
While Go has structs just like C does. Structs in Go again were designed in the right way. They support encapsulation. And encapsulation in Go was designed in a simple but yet very pragmatic and brilliant way — the way we name fields and methods matters. If a field or method begins with an upper case letter it is public and if it begins with a lower case letter it is private. This may seem strange at first glance but it’s actually a great decision made by Go’s creators which boosts productivity and readability. As the creators have said in one of their early talks this turned out to be one of their best decisions. And in fact, it is. I absolutely love it!
Go has pointers just like C and C++ but Go does not have the arrow access operator (->) because Go does not need one. Pointers and structs in Go play very nice together, again thanks to the experience of the creators of the language. In Go fields and methods are accessed in the same way no matter if the object for which they are accessed is a pointer or not- with the dot operator. This great decision makes working with pointers just like working with references from other languages but it does not hide the pointers from the programmer.
Another great decision made by Go’s creators is that methods are only defined and not declared. And their definition is taken outside the struct definition. This definitely improves the readability of the code written in Go. Looking at the source code of the definition of a struct is super easy to see what data it carries and which part of the data can be accessed by the outside world and which is private. And then we have all the behavior and logic associated with a given struct just below its definition. And that's just great and makes everything super easy to follow and track.
Also for each method separately can be decided if it’s gonna be called on a value or a pointer receiver. Rember Go’s semantics are Call-by-Value? If the receiver of a method is a value type then when this method gets called on a particular object that object will be copied. However when the receiver is a pointer type then when the method gets called through a pointer then only the pointer will be copied which in most cases is a lot cheaper than coping the object the pointer points to.
So there are two obvious reasons why to choose a pointer receiver:
- We don’t want to pay the price for coping the whole object every time the method gets called;
- We want to modify some data.
There is also one more reason which I suppose is less obvious. We want to use a pointer receiver to distinguish from let’s say a valid object and an invalid object or object and no object. Maybe a typical example would be - objects that are costly to create or objects which can hold resources that are costly to hold for long period and we don’t need those resources all the time but rather from time to time (well obviously we need some reference to/description of each resource which is kept for the whole time we need access to the resource). And this is so easy to achieve in Go because we could just check if the pointer is nil or not inside the method. Maybe now it’s easy to see that Go does not have the Null Pointer References: The Billion Dollar Mistake. Once again I will say that Go was designed extremely well.
Interfaces in Go play a very central role - they allow the great abstraction which is everywhere in the standard library of Go. Also, interfaces enable polymorphism in Go and they are very very heavily used in Go! Interfaces in Go are consumed just like in Java for example. The only difference is that in Go types implement interfaces implicitly. This is because Go is built around the concept of structural typing. And this is great! It is great because it allows Go code to grow and change super easy and helps with readability. Remember what I means in SOLID? Well, it means Interface segregation and directly translates into the fact that interfaces should be small because “Clients should not be forced to depend on methods that they do not use”. And to what does this could lead in languages like Java? Well, it could lead to long lists of interfaces a class implements which reduces readability. But this is nothing compared to the fact that by explicitly stating that a particular class in Java implements some interface(s) we subscribe for a potentially not so easy to refactor code. Also if we really want to be 100% sure that a type implements a particular interface in Go we can write a compile-time check anywhere in the code to ensure it.
As I said already Go does not support inheritance. I hope we have all heard at least once to “always prefer composition over inheritance”. And yes in the mainstream languages that support the OOP style with inheritance we should! The problem with type inheritance at least in languages like C++, Java, and C# is that when used is really hard to do right from the start. But at the same time usually, as new features get added to the product or the code naturally grows comes times when whole type chains/trees have to be changed or refactored and this just slows the development of new features…
While Go does not support inheritance it allows for type embedding. We can add to a struct a field without a name and this will embed the type of the field to the type of the struct we are defining. When type embedding is used to embed a struct all of its public fields are directly accessible from the struct that embeds it (of course if the struct that is the carrier declares a field with a name that coincides with a name of a public field of the struct that is embedded the field of the embedded struct must be accessed by first accessing the embedded struct). The same goes for all the public methods of a type when it is being type embedded. In fact, type embedding serves as the automatic lifting of methods so that the developer does not need to do it manually.
With type embedding, we can achieve results similar to inheritance in other languages thanks to the structural typing while using composition and without creating type hierarchies. Isn’t that great?
The creators designed (static) arrays, dynamic arrays (slices), and strings in Go in a remarkably great way. First, the length of a (static) array is part of the type! This means that arrays with the same type of elements but different lengths have different types. And arrays as everything in Go are passed by value (copied) and they do not decay to pointers as arrays do in C and C++.
Dynamic arrays in Go are called slices because they really are (slices). Internally slices are represented by a struct that has 3 fields: a pointer to an array (memory segment), length (the current length of the segment/how much of the segment is currently used), and capacity (the total length of the segment). And because slices hold a pointer to the data from the perspective of Go code, slices are passed by reference (are reference types) with respect to the data they point to but without the drawbacks of really being references! The best part of the design of slices is that in Go we can slice a slice which will lead to memory reuse. And also a slice comes with all the data one would need: elements, length, and capacity. This boosts performance a lot while allowing for very flexible usage of slices. Because slices are very cheap to pass around since passing them to function (or method) results in copying just a pointer and two integers.
For me, slices were the right take on dynamic arrays but the truth is that strings in Go are designed even better. I would say that their design is …. I actually can’t find the right words to describe how exceptionally brilliant the design is. But it is! Strings in Go are not terminated by a zero character! Internally strings are represented in a very similar to slices way - a data pointer and a length and this is all that defines a string- the bytes that it holds and its length, nothing more. So strings just like slices are very cheaply passed around, they feel like a reference type, and they allow for memory sharing because we can slice a string and in Go and strings are immutable.
After talking about arrays, slices, and strings in Go the logical continuation is the fact that every type in Go has a zero value. The zero value of a type is the default value for that type. For booleans this is false, for numbers is 0, for pointers, slices, strings, maps, interfaces, channels. and functions this is nil, typed accordingly. The zero value of a struct is an object whose fields are all set to the zero values of their respective type (the product of zeros in a structural manner). For arrays the zero value is an array of zero values. And usually, the zero value of a type is a ready-to-use (meaningful) value for the type. Such is the case for mutexes in Go for example.
Maybe one of the things for which Go is most know is the line “Don’t communicate by sharing, share by communicating”. This is because in Go we are encouraged to Share Memory By Communicating. And for this, we all have to thank Tony Hoare for his genius Communicating Sequential Processes (CSP) model and of course the creators of Go! And the 4 of them have my honest ad true thanks for what they have done! Go concurrency model is based on the CSP model but also on the famous Go-routines which are quite different from the concept of Co-routines from other languages.
Because this blog post is too big I will not say anything for Go-routines or channels and the select statement other than that they 3 together are what makes concurrency in Go awesome and very accessible and somewhat easy to do (well a lot easier than in most other languages). Instead, I’ll talk about them in a separate blog post, because they totally deserve it!
In this post, I’ll also not say anything about the Reflection in Go (I believe in Reflection, it's a good language feature to have) or the great tools around Go. I’ll only say that Go has remarkably good tooling around it. And I plan on writing a whole blog post dedicated only to tools in Go because they are really that great and they boost productivity a lot! But here I will thank all the engineers that built those great tools, thank you all!
I’ll end this section with Go code for creating generators that make their computations in separate Go-Routines.
The last feature of Go I’ll say something about are the packages (and modules). Originally Go code was organized only in packages. And packages were added to the languages as a way to address the biggest problem of C++ for Google at the time of the creation of Go which was the slow compilation of C++ code and the need to re-compile header files thousands of times (and at Google, they were hitting the limits of compilation farms). Latter on Modules were added to the language as a way to group packages for publishing.
A package in Go can consist of multiple files (it is not limited to a single file) which allows for “one piece of functionality per file” which is great. Packages don’t export everything inside them. As one would guess the developer is the one who chooses whats gets exported and what not. And this is done the same way as the way struct fields are made public or private or methods of a type are - through naming. Every type, variable, constant, or function is exported if its name starts with an upper case letter. Again this turned out to be a great decision because in Go we don’t need to carry in our heads information of what is public or not and what is exported. This is easily seen everywhere just by looking at the code!
I would like to thank each reader of this post that got this far, thank you I hope you liked what you’ve read! And that you will also read and enjoy the posts which I’ll write next!