Why Go is a poorly designed language

Ian Byrd
7 min readOct 28, 2015

--

Alright, the title is quite bold, I admit it. I’ll tell you more: I love bold titles, they are all about attention. Anyway, in this blog post I’ll try to prove that Go is a terribly designed language (spoiler: in fact, it is.) I’d been playing with Go for a couple months already and run my first helloworld somewhen in June, I think. I am not particularly good at math, but it’s already been four months since then and got a few packages on Github already. Needless to say, I also had absolutely no experience of using Go in production, so take my sayings about “code support”, “deployment” and related.. with a reasonable grain of salt.

I love Go. I loved it since I tried it. It took me a few days to accept idiomatics, to overcome lack of generics, keep it up with the weird error handling and all these classical Go issues. I read Effective Go, many articles on the Dave Cheney’s blog, kept track of everything related to Go and so on. I could say that I am an active community member! I love Go and I just can’t help it—Go is amazing. Still, in my humble opinion, Go is a terribly, poor designed language, which does exactly opposite to what it advertises.

Go is considered a simple programming language. According to Rob Pike, they took everything out of the language, making its spec just trivial. This side of the language is amazing: you can learn basics in hours, get into real coding straight away and it’d work, in most cases Go works just as expected. You’d be pissed off, but hopefully it works. Reality is quite different, Go is not a simple language, it’s just poor. Here are some points proving it.

Reason 1. Slice manipulations are just wrong!

Slices are great, I really like the concept and some of implementation. But let’s for a whole second, imagine, that we might actually want to write some source code with them. Obviously, slices live in the heart of the language, they are what makes Go great. But again, let’s imagine that just occasionally, in between “concept” talks, we’d want to write some real code. The following code listing is how you do slice manipulations in Go.

Believe it or not, that’s how Go programmers transform slices every day. And we don’t have any sort of generics, so you can’t create a pretty insert() function that would hide this horror, mate. I posted this on playground, so you shouldn’t trust me: you can double-check it on your own.

Reason 2. Nil interfaces are not always nil :)

They tell us that “errors in Go are more than strings” and that you shouldn’t treat them as strings. For example, spf13 from Docker said so on his adorable “7 common mistakes in Go and when to avoid them” talk.

They also say that I should always return error interface type (consistency, readability, etc). That’s what I do in the following code listing. You’d be surprised, but this program will indeed say hello to Mr. Pike, but is it really expected to?

Yeah, I am aware of why this occurs, since I read a bunch of comprehensive resources about interfaces and how they work in Go. But for a newcomer… come on, guys, it’s a hammer! In fact, it’s a common pitfall. As you can see, Go is such a straight-forward and easy-to-learn language with all the distractive features taken out, it sometimes says that nil-interface is not nil ;)

Reason 3. Funny variable shadowing

Just in case if you are not familiar with the term, let me quote Wikipedia: “variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope.” Seems legit, quite a common practice, most languages support variable shadowing and it’s just fine. Go is not an exception, yet it’s different. That’s how shadowing works here:

Yeah, I am also aware that := operator creates a new variable and assigns a right-hand value to it, so according to language spec it’s an absolutely legit behaviour. But here is a funny thing: try removing the inner scope—it’d work just as expected (“after 42”). Otherwise, say hello to variable shadowing.

Needless to say, it’s not just some funny example I came up with during lunch, it’s a real thing people run into eventually. I’ve been refactoring some Go code earlier this week and run into it twice. Compiler is fine, linters are fine, everybody is fine—code is not working.

Reason 4. You can’t pass []struct as []interface

Interfaces are great, Pike&Co. keep on saying that it’s what Go is: interfaces is how you workaround generics, it’s how you do mock testing, it’s the way polymorphism implemented. Tell ya, I loved interfaces with my heart right while reading “Effective Go” and I keep on loving them. Except “this nil interface is not nil” issue, I addressed above, there is another nasty thing which make me think that interfaces do not have a first-class support in Go. Basically, you can’t pass a slice of structs (that satisfy some interface) to a function, recieving slice of this interface type:

Unsurprisingly, this is a known issue, which is not considered an issue at all. It’s just a yet another funny thing about Go, alright? I really recommend you to read a related wiki on point, you’d find out why the “passing struct slice as interface slice” won’t work. But hey, just think about it! We can do this, there is no magic, it’s just a compiler issue. Look, I did an explicit conversion from []struct to []interface on lines 49–57. Why can’t Go compiler do this for me? Yeah, explicit is better than implicit, but wtf!?

I just can’t stand how people look at this sort of bullshit language is full of and keep on saying “yeah, it’s just fine”. It is not. It’s what makes Go a horrible language.

Reason 5. Non-obvious range “by-value” loops

This is the first language issue I ever encountered. Alright, so there is a “for-range” loop in Go, it’s there to range over slices and listen to channels. It’s used everywhere and it’s just fine. Here is still a minor issue though, most newcomers keep on failing on: range loops are by-value only, it copies values and that’s it, you can’t really do anything about it, it’s not foreach from C++.

Note, I do not compain about Go missing by-reference ranges, I complain about ranges being non-obvious. Verb “range” kinda says “iterate over items”, it doesn’t really say “iterate over items’ copies”. Let’s take a look at For from “Effective Go”, it says nothing like “range copies values from the slice”, it just doesn’t. I agree that it’s a minor issue, I got over it pretty quickly (minutes), but unexperienced gopher might spend some time debugging a chunk of code, wondering why values do not change. You guys could at least elaborate the point in “Effective Go”.

Reason 6. Questionable compiler rigidity

As I could have told you before, Go is considered a clear, simple and readable language with a strict compiler. For instance, you can’t compile a program with an unused import. Why? Just cos’ Mr. Pike thinks it’s right. Believe it or not, unused import is not the end of the World, I can totally live with it. I totally agree that it’s not right and compiler must print a related warning, but why the hell would you terminate compilation because of such a minor thing? Unused import, really?

Go1.5 introduced a funny language change: now you may list map literal omitting the contained type name listed explicitly. It took them five (or even more) years to realize that explicit type listing might be excessive.

Another thing I particularly enjoy in Go is readability thing: commas. You see, in Go you are free to define multiline import / const and var blocks:

import (
"fmt"
"math"
"github.com/some_guy/fancy"
)
const (
One int = iota
Two
Three
)
var (
VarName int = 35
)

Alright, it’s just fine. But once it comes down to “readability”, Rob Pike decided that it might be a great thing to add commas. At some point, after adding commas, he decided that you should keep the trailing comma as well! So instead of writing this:

numbers := []Object{
Object{"bla bla", 42}
Object("hahauha", 69}
}

You must write this:

numbers := []Object{
Object{"bla bla", 42},
Object("hahauha", 69},
}

I still wonder why we can omit commas on import/var/const blocks and just can’t on lists and maps. Anyway, Rob Pike knows better than me! Viva la Readability!

Reason 7. Go generate is too quirky

First of all, note that I do not have anything against code generation. For a poor language, like Go, it might be the only viable way of omitting some copy-pasting for a generic sort of stuff. Still, go:generate—a code generation tool, which is being used by Go folks all over the World now is just rubbish. Well, to be fair, tool itself is fine, I like it. The whole approach is just wrong. Let’s see, to generate some code you are supposed to use special magic comment. Yeah, some magic sequence of bytes somewhere in the code comments leads to code generation.

Comments are meant to explain code, not generate it. Still, magical commenting is a thing in today’s Go. Interestingly, noone actually cares, it’s just fine. In my humble opinion, it’s definitely much worse than freaking unused imports.

Epilogue

As you can see, I didn’t complain about generics / error handling / syntactic sugar / other quite classical Go-related issues. I agree that generics are not critical, but if you take away generics, please give us some normal code generation tools, not some random punky funky magical comment shit. If you take away exceptions, please give us ability to safely compare interfaces to nil. If you take away syntactic sugar, please let us write the code that works as expected, without some “oops haha” variable shadowing stuff.

All in all, I’ll continue to use Go. For a good reason: just because I love it. I hate the language: it’s absolute crap, but I love community, I love tooling, I love fancy design decisions (hey interfaces) and the whole ecosystem.

Hey man, wanna fork Go?

--

--