Go is not (very) simple, folks

I’ve recently started coding a little bit in Go, mostly out of curiosity. I’d known quite a bit about it beforehand, but never tried it out in practice (there was no need). But now Go is being considered as one of the options for a project in the team where I work and so I thought it would be nice to get a bit of hands on experience.

I’ve been having a fairly good time so far with the language itself. I’ll write more about where I think Go’s true strenght is towards the end of this blogpost.

What has been substantially less pleasant is the community, or more specifically, people who advocate Go because of its purported simplicity. It seems that simplicity has become a bit of a meme/buzzword in the Go community with many people repeating it over and over and over again without giving it much actual thought.

This seems quite unfortunate to me because Go being an “extremely simple language” is in my opinion:

  1. Not the main reason one should consider using Go
  2. Pushes other good reason out of the attention they deserve
  3. Not even actually true

In this article I’d like to have a look at many of the simplicity claims thrown around Go.

Before diving in, I’d like to stress one thing: This article is not meant to be a criticism of Go, but rather of the way Go is advertised and advocated. Sometimes I may critize an aspect or another of the language, but that is not the focus here and I’ll try to mention those only in a casual, matter-of-fact, every-language-has-those manner.

Where I come from

I use multiple programming languages both professionally and as a hobby. I’m not in favour of the concept of having “a favourite language”. I used to have some favourite languages in the past, but it was always only a matter of time to recognize that sentiment is usually better left out of the question.

Right now as part of my job I code in C++ and Python on a backend of a large service. In the past I used to work on an OS that you probably know and I’ve done embedded work as a contract as well. I do various other things as part of hobby projects.

I’m not saying all of that to boast or anything (I’m no one special), I only mean to show that I have at least some appreciation for many areas of programming and I’m trying to keep an open mind.

So, without further ado, let’s get down to bussiness and have a look at a couple of claims.

1. “Go has very few keywords compared to mainstream languages”

I’m starting with the most glaring examples and this one is definitely my pet peeve when it comes to Go advocacy.

First of all, even it were actually true, I have no idea why keyword count is any important to the learning curve or complexity of a language. Sure, if there were thousands of keywords, that might be a problem. But most languages have more or less some few dozens of keywords at most and at that scale it is fairly insignificant how many exactly are there.

I haven’t yet heard anyone complain about some language because of a number of keywords.

Secondly, Go’s “low” keyword count is in fact nothing more than a clever lawyer’s trick (perhaps I’d even go as far as ‘false advertising’). The Go specification lists 25 keywords, which is somewhat lower than most other languages, however, it seems to me that rather than having a lower number of concepts that are represented by keywords in other languages, Go simply doesn’t have keywords for those concepts, yet still actually features them as part of the language (ie. the actual complexity remains the same).

To illustrate what I mean, consider a while loop. Go doesn’t have this keyword, so much is true, but it still has a while loop, the documentation even says so, it just reuses another keyword for the purpose.

Another such example is private and public. Go doesn’t have these keywords, but it still does have private and public, it just uses letter casing rather than keywords in the syntax for it.

Another trick used to shave off keywords is called Predeclared identifiers, which technically aren’t keywords, but they are still needed in practice and it still isn’t a good idea to create variables named the same, so, at the end of the day… they basically are keywords. Furthermore, some of these predeclared identifiers are keywords in other languages and so it is quite unfair to compare them with Go’s keyword list only. Apples to oranges.

2. Receiver argument

The receiver argument is bit of an oddity to me. Seems like Go didn’t want to have this or self, but still wanted methods, so it has a ‘receiver argument’, which is basically the same thing except it makes methods signature look weird.

My problem with the receiver argument is that when reading a method, I need to know the name of the receiver arg (which is arbitrary) to see where the method acts on the receiver. Syntax highliting is hindered by the lack of a keyword. (See? This is how reduction of keywords actually makes things more complex.) This is kind of like with implied this in C++.

And indeed there’s an example of a newcomer being confused by this.

IMHO the simplest and most straightforward way to express a receiver is the UFCS rather than what C++ or Go do. But like I said, I’m not complaining about Go here, I don’t really mind the receiver argument (if I can’t put up with C++’s weirdness, I can put up with Go’s).

3. Function return values

As if the receiver argument wasn’t enough, the function signuature can get even more baroque with various sorts of return value declarations. A typical language will let you return one value from a function with a return statement. In Go, you can additionally return multiple values (which I think can be solved in a more elegant way with tuples, but alright) and on top of that, there’s Named return values. Which IMO is not a very good idea, because it allows people to write confusing code where it is hard to find what the function actually returns. Combined with the receiver argument, you can create a function signature like this:

func (f Foobar) Something(a int, b int, c int) (foo int, bar int) {
// ...
}

… and that’s valid Go code. As you can see, there’s three argument lists. I really would not like anyone to try to sell this to me as “simple” because this syntax is nothing but simple.

4. “Not inheritance”

Go (or perhaps just the community?) seems to be pretty opposed to the “traditional OOP” (whatever that means — probably Java or C++). I’ve seen people claim that it’s a good thing Go doesn’t have that.

Except it does. Go has a feature called embedding which the documentation as well as several blogposts claim is not inheritance. I’ve tried using it in various ways and I can’t really shed off the impression that it’s just rebranded inheritance. The documentation linked above says:

There’s an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.

What’s the difference? Inheritance typically works exactly the same way, the inherited methods also act on the inner type.

IMO really the only difference is that in Go, polymorphism is decoulpled from structures, ie. you need to use interfaces to bring polymorphism to the play. But once you do, you can do things very similar to traditional OOP, including method overriging — here’s a demonstration.

One thing that really surprised me about Go — a supposedly simple language — is that you can even do multiple inheritance. And it’s pretty bad. Someone on the golang-nuts mailing list discovered, that Go doesn’t handle inheritance ambiguities very well. I have adapted the code mentioned therein so that it also showcases the well-known ‘Dreaded diamond problem’:

package main
import "fmt"
type T1 struct {
T2
T3
}
type T2 struct {
T4
foo int
}
type T3 struct {
T4
}
type T4 struct {
foo int
}
func main() {
t2 := T2{ T4{ 9000 }, 2 }
t3 := T3{ T4{ 3 } }
  fmt.Printf("foo=%d\n", t2.foo)
  fmt.Printf("foo=%d\n", t3.foo)
  t1 := T1{
t2,
t3,
}
  fmt.Printf("foo=%d\n", t1.foo)
}

Here’s the same code in Go playground.

It blows my mind that the code above compiles without any warnings/errors. Here’s an analogous code in C++, you can see that it didn’t compile because of the ambigous field.

What’s the aftermath? Well, first of all, I think featuring multiple inheritance pretty much precludes the usage of the word “simple” anywhere in the description of the programming language in question. No one can convince me that Go is one of the simplest languages out there or maybe even a simple language after I’ve seen the code above. And it doesn’t even feature some of the other things you can do with embedding, such as embedding by pointer or embeding interfaces by pointer. (I’m not even sure what all the implications of those features really are.)

Secondly, I’d like to make a brief exception here and critize the Go language itself. Not handling the ambiguity like that seems like a design/implementation bug. Not even C++ is crazy enough to compile such code without a complaint, and that should tell you something.

5. Error handling

Various kinds of error handling are typically a huge flamewar material. I don’t want to get into that. I’ve used all common error handling styles (I think) in different languages at one point or another and I dislike all of them equally with a passion. I’m of the opinion that error handling is always going to be a PITA no matter what. By trading one style for another you just trade one set of problems for another. There’s no silver bullet.

Back to the topic of simplicity: Go made the choice not to use exceptions, which does make things somewhat simpler I suppose. What does not make things simpler is the multiple return values feature, which means that instead of returning either an error or a succes value, you can return both of them or neither (in CS lingo you could say the problem is the usage of a product type rather than a sum type). Indeed, I’ve seen multiple code reviews of newcommer’s code where the person ran into this footgun.

If Go didn’t allow multiple return values and instead there were some suitable sum/either-like type, that would make things much simpler IMO. For very much the same reason, it is fairly easy to ignore errors in Go and/or to fail to report them to the caller or other appropriate destination properly.

Another thing that makes matters not simple is panicking. Don’t get me wrong, I understand the reasons why it exists in Go and how it is useful, in fact, a bunch of other languages have a similar arrangement. I’m just mentioning it as a point against the simplicity claim — A newcommer is IMHO likely to be confused as to what the difference between the two facilities is and which one is appropriate when.

6. Generics

This topic might be an even bigger can of worms than error handling.

Like with errors, I’d just like to consider the complexity / simplicity aspect here. Many people in the Go community seem to believe that generics are inherently complex (= bad, mmkay) and have a huge overhead of one kind or another. This is true to some degree, but I don’t think it’s anywhere near as bad as some people like to portray it. It almost seems as if those people have had a painful experience with C++ templates and since then get a PTSD attack whenever someone mentions generics.

Listen here folks, generics are not a bogeyman. They absoutely don’t need to be as complex as they are in C++ (or some other bogeyman language). I mean, even the frontend guys work with generics these days (TypeScript, Flow, …) and if they are not scared of generics, there’s no reason any other programmer should be :) (Sorry frontend devs, just joking.)

People also don’t realize that if used sensibly, generics can make matters much simpler for users of many types and functions. For example, consider Go’s Heap interface. This is how you pop from a heap implementing this interface:

popped := heap.Pop(&someheap)
myfoo := popped.(*Foo) // ZOMG what just happened here?

Picture explaining this to a newbie, including the risk of panic. Also perhaps consider what will happen if they don’t really get the whole interface{} thing right. Compare that to:

myfoo := heap.Pop(&someheap)   // myfoo has the correct type

This is easier to read, easier to explain (you explain it just like you would explain the map type that is already present in Go!) and also harder to mess up when writing code.

The lack of generics is the thing that causes additional complexity here, and it causes quite a bit of additional complexity in other parts of Go as well, mostly by requiring the presence of various “magical” functions/types. The map, slice and channel types are magical, as well as the accompanying make() function, which is a constructor for all three of them. The slice type works as both a reference to an array and a dynamic array. (Whatever happened to “Do one thing and do it well”?)

(Just to remind everyone, I don’t really mind any of that, just mentioning it for the sake of the not-simple argument.)

7. Miscellaneous

I think I’ve gotten the main simplicity violators out of the way. There’s just a couple of minor ones left on my list:

  1. The <- and -> operators. These could’ve easily just been methods of the channel type.
  2. iota — basically just like enums but weirder.
  3. Built-in complex numers
  4. If with a short statement (which might be useful sometimes, but makes if syntax more complex than what I’m used to from other languages)

… that’s about it I think. Might’ve forgotten about something, but I think we’ve had enough.

So, what do I think Go actually brings to the table if not simplicty?

Tasks — “goroutines”

This might seem sort of obvious / expected, since goroutines are an often-mentioned feature just like “simplicity” and so I feel there’s a need of a distinction: I don’t think it’s concurrency in the usual sense that constitutes the strenght of Go. Don’t get me wrong, Go’s concurrency is alright. It’s just not somehow special in any way. You have channels, which are sure nice, but basically they are just concurrent queues like I’m used to elsewhere. And then you have the usual palette of concurrency primitives like mutexes, read write locks, condition variables and the like. You can synchronize your stuff and you can get race conditions and deadlocks exactly like with many other languages.

What I like about goroutines (besides the obvious fact that they are lightweight userspace threads) is the way they are usable with I/O — the way the scheduling is connected to the low-level I/O API of the host OS (epoll, kqueue, IOCP, …). This is something that is typically quite hard to make pleasant and wieldy for the programmer, especially in compile-to-native languages. I’m still learning about the details here, but it definitely seems to me this is the bit that is done pretty well and the reason why I consider Go as a canditate for a future project.

As already hinted, I also like the fact that Go compiles to native code. It’s nice to see new languages preserving this desptive using garbage collection (or other forms of automatic memory management — Swift comes to mind).

Conclusion

So, where does all of that leave you, the reader? Is Go complex or what?

Well no, definitely not as complex as languages like C++ or Haskell. In comparison to those, Go is indeed simple. On the other hand, when comparing the complexity of Go and other common languages like Java, JavaScript, Python, etc., things are much less clear, as I hope I’ve shown. (Besides, it is a hard and not well-defined task.)

I can offer the takeaway that is that it’s about similar. In some aspects Go might be simpler than those languages, in some not so much… By and large I’d say it’s about the same on average as other common languages. Also I don’t think the simplicity, whether perceived or actual, is really that important at the end of the day anyway. I don’t think it is.

Finally, where does this article leave me, the writer? I’m not sure yet. I don’t know yet if Go will be chosen for a (sub)project in my dayjob or if I’ll perhaps use it in a hobby project or not. I’d like to avoid the part of the community that pushes upholding the kind of dogma mentioned in this article. Is there perhaps a place where somewhat less ideologically oriented Go people hang out? Feel free to advice me on that.

I have the same problem with the Rust community, mind you, where I’ve also learned it’s better to stay away from the more fanatic proponents. (Q: “Can you rewrite your project in Rust?” A: “Get lost.”) Perhaps it’s the nature of these new languages and their struggle for a spot of sunlight that motivates people to be like that.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.