Today marks six months since I started working with Go (also known as GoLang). Before that, I worked for about a decade using C#, with which I became quite comfortable over the years. It’s been fun to learn a new programming language professionally, but it does take some adjustment. After six months, I don’t expect to be an expert, or even know the language well, but I’d like to share the candid experience of a newcomer to programming in Go.
The general feeling I have about Go is that it is somewhat tedious to work with (see also my Twitter thread from 3 months ago). This is down to a lack of (a) language features that make development more productive, and (b) standard library functionality that provides common things that everybody uses. Perhaps some of this might be down to my own inexperience, and I welcome feedback as long as it’s constructive. However, my understanding (e.g. based on common Stack Overflow answers) is due to Go’s nature as a “simple language”.
I don’t buy the “simple language” argument (we’ll see more of this later). Why would Google bother to create a new language that offered less features than existing languages? Thinking about it, it’s probably down to historical reasons. Go seems to have appeared late in 2009. At the time, there weren’t a lot of options in terms of robust, productive, general-purpose programming languages that were both open-source and cross-platform. Node.js was still in its infancy; Rust would only be released the following year; and .NET Core was still several years away, which means C# was still restricted to Microsoft platforms. Python had been around much longer, but it has some very well-known limitations (e.g. in terms of performance).
In the rest of this article, we’ll take a tour of some of the things that stood out as I’ve been learning and using Go. I’m just hoping this will help illustrate why I think Go is tedious, and perhaps help other people thinking about picking up the language.
Update 16th November 2022: see also the followup article: From .NET to GoLang: Here We Go Again.
OOP and Classes
Let’s get this out of the way first: Go is a procedural language, not unlike C or Pascal. You write a bunch of statements, control the flow with loops and conditional statements, organise them into functions, and that’s about it. There are no classes, objects, methods and all that (although there is some concept of interfaces, and receiver functions seem very much like extension methods).
To be honest, this is the one thing that doesn’t bother me at all. I’ve seen countless developers overcomplicate life unnecessarily with OOP (e.g. layers upon layers of inheritance) in C#. I’m not all-out against OOP as Zed Shaw is (see “Object Oriented Programming in Python“). However, the vast of majority of work I’ve done with C# was working with data (whether that’s building an API, working with a cache, using queues etc, it’s almost always a matter of getting data, transforming it, and passing it somewhere else) which doesn’t seem to need abstraction, so a procedural approach fits. OOP is better suited for modelling more complex things like GUI elements, games, etc.
It’s interesting to note that while Go doesn’t have OOP, this didn’t quite spare it from the horror of ORMs (e.g. GORM). (See ADO .NET Part 1: Introduction for why I’m not a fan of ORMs.)
Generics
One of the things developers missed most since Go’s inception were generics. While Go does have some generic data structures (e.g. the map
), it didn’t allow developers to create their own generic data structures until support for generics was added to the language in Go 1.18 in March this year. This means that, for instance, if you wanted to create your own stack data structure, you’d have to create one stack for integers, another for strings, etc.
Even now that generics are available, the fact that they haven’t been around long means there are limitations. A couple I’ve run into include:
- You can’t have generic type aliases.
- Gin Swagger doesn’t support generating Swagger docs for generic structs.
If you’re familiar with the history of .NET, you might recognise that even C# initially shipped without generics in 2001, and they only made it into the language in C# 2.0 (2005). However, it’s taken Go 12 years to get generics, and we’re now in 2022.
Standard Library
Sets
One thing I use a lot is a set data structure. In C#, Python or JavaScript, you get this out of the box. But Go doesn’t have it. Why not?
Well, someone asked this on Stack Overflow back in 2015. As is typical for Stack Overflow (see “On Stack Overflow“), the question got closed. The top answers are variants of “it doesn’t have a set data structure because you can write it yourself”.
This attitude infuriates me. Software development is complex enough, and I’d like to focus on whatever problem I need to solve, instead of reinventing the wheel and going on a yak shaving spree every time I need some common dependency that the standard library doesn’t provide.
Besides, it’s not as simple as writing a set data structure. You also need to write implementations for the operations you need (e.g. intersection, union, difference, symmetric difference, etc), test them thoroughly, make sure they’re efficient (from a performance perspective), etc. This is something that takes time to get right, but at the same time it’s also something basic that’s already been solved to death, and there’s no reason why every Go developer should have to reimplement it, when other languages provide battle-tested implementations out of the box.
In fact, there’s a comment on one of the answers that echoes my frustration:
“The usual answer for golang question: “Why provide a feature when you can rewrite it in just a few lines?”. This is why something that can be done in 3 explicit lines in python (or many other languages) takes 50+ obscure lines in go. This is one of the reasons (along with single letter variables) why I hate reading go code. It’s uselessly long, just doing with for loops what should be done by a clear, efficient and well tested properly named function. Go “spirit” is just throwing away 50 years of good software engineering practice with dubious justifications.”
— Colin Pitrat, Jul 8, 2021 at 11:34
Go doesn’t come with much other than arrays, slices and maps. However, in 2017, with the release of Go 1.9, it did get sync.Map, which I understand is similar to the ConcurrentDictionary in .NET. For anything else, you’ll likely have to find an implementation on GitHub or write it yourself.
LINQ
I already said Go is a procedural language. You’ll feel it a lot. For everything you need to do, you’ll have to write lots and lots of loops, making the code a lot more verbose and error-prone compared to other languages where you can use a more functional approach (e.g. C# LINQ Select()
, map()
in JavaScript or Python, or list comprehensions in Python).
Mathematical Functions
If I want to find the smallest number in an array in C#, I just call Math.Min()
.
Does Go have a built-in function to get the smallest number in an array? No, you have to write it yourself. Here we go again.
Update 30th October 2023: min()
and max()
built-in functions were finally added recently in Go 1.21.
Exception Handling
I never really liked exception handling in OOP languages. I felt that checking return values of functions as in C was a lot more clear that what seemed to be a wrapper for a goto
-like construct where code could suddenly jump elsewhere unpredictably on a whim.
Go doesn’t have exception handling, and so most logic looks something like this:
func doSomething() error {
foo, err := doSomethingElse(1)
if err != nil {
logrus.Error("Step 1 failed", err)
return err
}
bar, err := doSomethingElse(5)
if err != nil {
logrus.Error("Step 2 failed", err)
return err
}
chicken, err := doSomethingElse(10)
if err != nil {
logrus.Error("Step 3 failed", err)
return err
}
// ...
return nil
}
I changed my mind. I want exception handling back.
Unused Variables
The code in the previous section doesn’t actually compile. Why not?
$ go run main.go
# command-line-arguments
./main.go:14:2: foo declared but not used
./main.go:20:2: bar declared but not used
./main.go:26:2: chicken declared but not used
Go actually fails to build if you have unused variables.
While I totally understand the benefit of keeping code clean, this is simply extreme, and very irritating. It’s very common for me to need to add a temporary variable to capture the output of some computation (or an HTTP request) and see what’s in the data, but in Go, I have to resort to a redundant fmt.Println()
just to mark the variable as in-use and keep the compiler happy. It’s much more suitable to issue a warning than to fail the build.
Syntax
Function Overloading
Go doesn’t have function overloading, so you can’t have different functions with the same name and different parameters. You’ll instead have to come up with silly variants of functions that do the same thing, e.g. doSomething()
and doSomething2()
. Feels like going back in time, doesn’t it?
if
Statements, Braces and Semicolons
if
statements in Go don’t need brackets around the condition, but do mandate braces around the statements, even if there is only one statement:
if chicken < 5 {
logrus.Info("Chicken is less than 5")
}
I’ve already written at length in “To Always Use Braces for Conditionals and Loops… or not” why I’m in favour of omitting braces for single-line statements. And since that’s not possible in Go, it only serves to add further verbosity to the language.
Braces must also be “Egyptian-style” (as shown above). The reason for this is explained in the FAQ and is down to semicolon insertion, just like JavaScript (which is very sad):
“Why are there braces but no semicolons? And why can’t I put the opening brace on the next line?
“Go uses brace brackets for statement grouping, a syntax familiar to programmers who have worked with any language in the C family. Semicolons, however, are for parsers, not for people, and we wanted to eliminate them as much as possible. To achieve this goal, Go borrows a trick from BCPL: the semicolons that separate statements are in the formal grammar but are injected automatically, without lookahead, by the lexer at the end of any line that could be the end of a statement. This works very well in practice but has the effect that it forces a brace style. For instance, the opening brace of a function cannot appear on a line by itself.
“Some have argued that the lexer should do lookahead to permit the brace to live on the next line. We disagree. Since Go code is meant to be formatted automatically by gofmt, some style must be chosen. That style may differ from what you’ve used in C or Java, but Go is a different language and gofmt’s style is as good as any other. More important—much more important—the advantages of a single, programmatically mandated format for all Go programs greatly outweigh any perceived disadvantages of the particular style. Note too that Go’s style means that an interactive implementation of Go can use the standard syntax one line at a time without special rules.”
— Go FAQ
Ternary Operator
Go doesn’t have the ?:
ternary operator, often used in other languages as a concise replacement for an if
statement. Why not? Once again, the question that asks this on Stack Overflow has been closed, but the answer quotes the Go FAQ to shed some light. The reason is a variant of “some developers have made messes with the ternary operator, and that’s why you can’t have nice things”. Come. On.
Loops
When it comes to loops, Go has just the for
loop, so it doesn’t have the usual while
and do..while
loops you’d normally find in C-style programming languages. This doesn’t bother me, as I almost always use just for
loops anyway.
Go’s for
loop does, however, support a foreach
-style way of iterating over objects in an array. Let’s try a simple iteration over an array of odd numbers:
odds := []int{1, 3, 5, 7, 9}
for n := range odds {
fmt.Println(n)
}
There are a few things that bother me here.
- The syntax of the array declaration. We’ll get to this later.
- The for
n := range odds
part looks like it’s assigning an entire range to the variable, whereas what it’s really doing is something likeforeach (n in odds)
in C#. - It doesn’t print what you think it does! The first variable from a
range
assignment is the index, not the element, so the above code gives the output:
0
1
2
3
4
In order to print the elements, you have to introduce a second variable:
odds := []int{1, 3, 5, 7, 9}
for _, n := range odds {
fmt.Println(n)
}
Since the point of a foreach
is typically to work with an element in a collection, I much prefer C#’s orthogonal way of giving the element by default and having the index as optional.
Variable Declarations
Go’s syntax seems to be loosely based on C-style languages. It uses braces and a lot of syntax and operators are familiar, but it does make some very strange deviations. We’ve already mentioned earlier the lack of semicolons, but there are a couple of other differences that make the language more reminiscent of Pascal than anything else.
The first of these is the fact that type declarations go after the variable name in a variable declaration, e.g.:
var age int
This is strange both because of the redundant var
keyword and because it gets very confusing when you switch between Go and another language.
As with many languages today (including older ones like C++), Go can infer the type if you initialise it to a value, e.g.:
age := 5
This :=
syntax is the other thing that reminds me of Pascal. I don’t really get why it’s beneficial (I assume the reason is mostly academic), but on the other hand I have found it very annoying, as I often have to change between :=
and =
while I’m moving code around. It’s also quite tricky given the fact that many functions return multiple values, and you’ll typically assign the results to a combination of new and reused variables.
Where variable declarations get really confusing is when the data types are data structures. We’ve already seen the initialisation of an array… where the square brackets come before the type:
odds := []int{1, 3, 5, 7, 9}
What about a map
? This is a map
of string
to int
:
mapping := map[string]int{}
It’s really, really strange that the type of the value is not delimited by any operator. So this gets weird when the value can be of a complex type itself. For instance, how would you make a map
of string
to slice of string
? I suppose it would be something like this:
mapping := map[string][]string{}
What about a map
of string
to another map
of string
to string
? I’m guessing that would be:
mapping := map[string]map[string]string{}
This is really weird. I think C#’s syntax is much more readable, even for complex generic data structures.
Race Conditions
Most of the things I’ve talked about are things I find annoying in Go, but I want to wrap up with one feature I find really great.
Go has a way to automatically detect race conditions when you run a program with the -race
parameter. This is particularly nice because multithreaded programming is very tricky to get right precisely because of race conditions. Go does provide goroutines and channels as an alternative way of inter-thread communication, but they don’t fit every situation. And since Go does provide basic locking mechanisms but not much in the way of concurrent collections (as opposed to C#), synchronising access to critical sections of code is often necessary. When that happens, having -race
handy is a nice feature.
Conclusion
Coming from C#, learning my way around Go has been a fun diversion, but also something of a disappointment. Given the simplistic, verbose and sometimes confusing language, and the limited standard library, there’s no reason I can think of to choose Go for a new project over something richer and more mature like C#, which nowadays runs on any platform.
Outside of full inheritance, which features that make a language OO is Go missing?
If we take the big three, Go comes pretty close:
Encapsulation – fully supported
Inheritance – partial support
Polymorphism – fully supported
Thanks for sharing all this information so that I don’t have to learn all this the hard way. I am a veteran .NET developer and open source contributor and I often come accross GO repos and am tempted to learn it so I can contribute – but have held off. Looks like I made the right decision. I think if I wanted to invest some time into learning another language, at this moment in time, it would be Rust. Just because Rust seems to be something truly special in terms of its language features and design goals – meaning you would actually approach problems by writing different code than you would with other languages – as opposed to just writing the same old code just in different syntax!