Alright, the title is quite bold, I admit it. Iāll tell you more: I love bold titles! In this blog post Iāll try to prove that Go is a terribly designed language (spoiler: it’s not.) 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 in case if I do talk about it ācode supportā, ādeploymentā and related… make sure you take it with a reasonable grain of salt.
I love Go. I fell in love with it ever since I had tried it for the first time. It took me a few days-weeks to accept idiomatics and overcome lack of generics, keep it up with the weird error handling and all the classic Go issues. I read Effective Go, many articles on Dave Cheneyās blog, kept track of everything related to Go and so on. I’d say I instantly became a pretty 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 poorly designed language, which does exactly the 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 as much trivial as possible. This side of the language is amazing: you can learn the basics in the matter of hours, get into the real coding straight away and itād work: in most cases Go works just as expected. Youād be pissed off, but it’s here, working. Reality is quite different tho: Go is not a simple language (well it is but still), itās just poor. Here are some points proving it.
Slice manipulations are broken
Slices are great, I really like the concept and some of the 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 do really transform slices. We don’t have to do this everyday, but once we doāit’s fun time. And we donāt have any sort of generics (haha lmao), so you canāt create a pretty insert()
function that would potentially hide this horror.
Sometimes nil interfaces are not entirely nil
They tell us ā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 tell us that we ought to return error
interface instead of specific errors (consistency, readability, etc). Thatās what I’m gonna do in the following code listing. Youād be surprised, but the following program will indeed print Wtf?
.
I am perfectly aware precisely why this occurs: after a while around you realize how interfaces do really work and eventually wrap your heard around it. But for a newcomer… come on, guys, it is a hammer! As you can see, Go is such a straight-forward and easy-to-learn language with all the distractive features taken out, it obviously lets nil interfaces to be not entirely nil, just for your own debugging lulz! That said, this issue is even mentioned in the official FAQ section:
An interface value is nil only if the inner value and type are both unset, (nil, nil). In particular, a nil interface will always hold a nil type. If we store a nil pointer of type *int inside an interface value, the inner type will be *int regardless of the value of the pointer: (*int, nil). Such an interface value will therefore be non-nil even when the pointer inside is nil.
This situation can be confusing, and arises when a nil value is stored inside an interface value such as an error return:
ā FAQ
As you can see, according to Go team, this is not an issue, but an ordinary confusing situation, which is obviously just fine. Go is a straight-forward and easy-to-learn language with all the distractive features taken out.. including non-confusing interface implementation!
Funny variable shadowing
Just in case you are not familiar with the term, let me quote Wikipedia here: ā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 nowadays. Go is not an exception, yet itās different. Thatās how shadowing works here:
Yeah, I’m aware that :=
operator creates a new variable and assigns it a corresponding right-hand value, so according to the language spec itās absolutely legit. This sort of shadowing comes handy: you see, error handling in Go is done by returning error
interface as the second return value. There is absolutely no elegant way to let this without introducing this sort of a funky shadowing circus.
Needless to say, itās not just some funny example I came up with before breakfast, itās a real issue everybody runs into eventually. Iāve been refactoring some Go code earlier this week and run into it twice. Compiler’s fine, linters are fine, everybody is fine, except the code.
No first-class support for interfaces
Pike&Co. keeps on saying that interfaces are basically what Go makes Go itself: interfaces is how you work around generics, itās how you do mock testing, itās the way polymorphism’s implemented. Usually, interfaces are the answer to all your pains. Believe it or not, I loved the interfaces with my heart right off reading āEffective Goā and I’ve kept on loving them ever since. Although except the āthis nil interface is not nil sometimesā issue I addressed above, there is an 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 (items of which satisfy some interface) to a function, recieving a 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! Spoiler: under the hood, interfaces and structs have a diffeent memory layout; slice is a continuos memory junk of fixed capacity, you can’t do anything about it. Yet we can actually implement the desired behaviour, there is no magic, itās just a compiler issue. Look over there, I just did an explicit conversion from []struct
to []interface
! It does allocation though. Why canāt Go compiler do this for me? If it could, it’d likely do it without any (by generating type-specific functions). Yeah, explicit is better than implicit, but wtf, Go?
I just canāt stand how people look at this sort of rubbish, language is full of and keep on saying āyeah, itās *just fine*ā. It is not. Itās what makes Go a horribly designed language.
Questionable compiler rigidity
As I could have told you before, Go is considered clear, simple and readable language with a rather strict compiler. For instance, you canāt compile a program with unused imports. Why? Just becoz Mr. Pike and his friends think itās right. Unused import is not the end of the world: you most certainly can live with it just fine. 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 issue? Unused import, really? Yeah, it does make overall compilations slower, but do we really care about this while doing intensive coding iterations? We obviously could have a fancy warning (which you could turn off) and pre-commit hook that automatically removes all unused imports instead. Unfortunately, it’s just fine.
Go1.5 introduced a funny language change: now you may list map literal omitting the contained type name listed explicitly. Think about it: it took them five (or even more) years to realize that putting value type in map initialization list all over the place might be excessive. In fact, I’m real surprised they actually made it!
Another thing in Go I particularly enjoy is “readability” thing: commas. You see, in Go you can 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 great to add some commas. At some point, after adding commas, he’s decided you should keep the trailing comma as well! So thereās inconsistency in how weāre supposed to deal with punctuation:
It’s not a huge issue; to be entirely honest. It does irritate people from time to time, usually on big struct init scope tho. I still wonder why we can omit commas on import/var/const blocks and just canāt on lists and maps. Anyway, Rob Pike and the wild bunch of engineers at Google know better than me! Viva la Readability!
Go generate is a quirk
First of all, note that I’m not against code generation. For a language with Go sort-of poor type system, it might be the only viable way of omitting some copy-pasting for all the generic type of stuff. Still, go:generateāa code generation tool from the default Go shed is just silly. Well, to be fair, the tool itself is fine, I like it. It’s the whole approach which is just wrong. In order to generate some code you’re supposed to use some special magic comment. Yeah, some magic sequence of bytes somewhere in the code comments leads to code generation.
The thing is. Comments are meant to explain code, not generate it. Magical commenting is a thing in the modern Go tho. Interestingly, nobody actually cares, since itās obviously just fine (you wonder!). In my humble opinion, itās definitely much-much worse than the freaking imports.
Epilogue
As you can see, I didnāt complain about generics, error handling, syntactic sugar and/or 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 or make a fancier compiler, not some random punky funky magical comment bollox. If you take away exceptions, please give us an ability to safely compare interfaces to nil, without wondering whether it derived from another type or not. If you take away syntactic sugar, please let us write the code that works as expected, without some “confusing” interface magic.
All in all, Iāll continue using Go for good. I hate the language and find it pretty bland even for the problems it solves, but I love the community (with a slight exception of “golang bridge” comm or whatever they call themselves, which I had horrible experience with) and the tooling.
Hey man, wanna fork Go a little bit?
(powered by Disqus)