Three Months of Go (from a Haskeller's perspective)

Tags haskell, go, programming
Target Audience Haskell programmers & Go programmers.
Attention Conservation Notice This is mostly just an opinionated rant.

This summer I’ve been in­terning at Pusher, and have been writing a lot of Go. It’s been a bit of a change coming from a Haskell back­ground, so I de­cided to write up my thoughts now, at the end.

The Good

In­cred­ibly easy to pick up

There’s not a lot to Go, it’s quite a small lan­guage. I had never written a line of it be­fore June, and now I’ve written about 30,000. It’s very easy to get started and be­come pro­duct­ive.

Haskell, on the other hand, is no­torious for being hard to learn (cough monad tu­torials cough). People often find it really hard to take the step from eval­u­ating pure math­em­at­ical ex­pres­sions to writing ac­tual pro­grams. I ex­per­i­enced no such dis­con­nect in Go.

Garbage col­lector keeps get­ting better and better

Pusher pre­vi­ously tried to use Haskell for the pro­ject I was working on, but even­tu­ally had to give up due to un­pre­dict­able latency caused by garbage col­lec­tion pauses. GHC’s garbage col­lector is de­signed for through­put, not latency. It is a gen­er­a­tional copying col­lector, which means that pause times are pro­por­tional to the amount of live data in the heap. To make mat­ters worse, it’s also stop-the-­world.

Go’s garbage col­lector is a con­cur­rent mark-­sweep with very short stop-the-­world pauses, and it just seems to keep get­ting better. This is def­in­itely a good thing for a garbage col­lected lan­guage. We did have some is­sues with un­ac­cept­able laten­cies, but were able to work them all out. No such luck with the Haskell pro­ject.

Style wars are a thing of the past

Say what you like about gofmt, but it makes ar­gu­ments over code style al­most im­possible. Just run it on save, and your code will al­ways be con­sist­ently format­ted.

I do find it a little strange that gofmt has been com­pletely ac­cep­ted, whereas Py­thon’s sig­ni­ficant whitespace (which is there for ex­actly the same reason: en­for­cing read­able code) has been much more con­ten­tious across the pro­gram­ming com­munity.

The Neutral

Code gen­er­a­tion seems to be the ac­cepted solu­tion to a lot of prob­lems

I am not a huge fan of code gen­er­a­tion (and I say this as the au­thor of a code gen­er­a­tion tool). I think it can do good, but it can also ob­scure what’s ac­tu­ally going on. In every dis­cus­sion on Go gen­er­ics, someone will come along and say you can add gen­erics with code gen­er­a­tion: that’s true, but at the cost of in­tro­du­cing ad­di­tional, non­stand­ard, syn­tax.

I sus­pect the strong cul­ture of code gen­er­a­tion is largely be­cause it lets you work around the flaws of the lan­guage.

Strict, not lazy, eval­u­ation

Strict eval­u­ation is typ­ic­ally better for per­form­ance than lazy eval­u­ation (thunks cause al­loc­a­tion, so you’re gambling that the com­pu­ta­tion saved off­sets the memory cost), but it does make things less com­pos­able. There have been a couple of times where I’ve gone to split up a func­tion, only to realise that doing so would re­quire al­loc­ating a data struc­ture in memory which be­fore was not needed.

I could trust the com­piler to in­line things for me, and so op­timise away the ad­di­tional al­loc­a­tions, but in a lazy lan­guage you just don’t have that issue at all.

The standard lib­rary is not so great

If you know me in per­son, it might seem a little odd that I spe­cific­ally com­ment on this. Nor­mally I am all for lan­guages having a small, really well-writ­ten, stdlib and everything else provided through lib­rar­ies. I am picking on Go here a bit be­cause the standard lib­rary seems to get a lot of praise, but I was un­im­pressed.

Parts of it are good, a lot of it is me­diocre, and some of it is down­right bad (like the go/ast package doc­u­ment­a­tion). It seems a lot of Go’s use is in web­dev, so per­haps those bits of the stdlib (which I haven’t touched at all) are con­sist­ently good.

The Bad

I also agree with this Quora an­swer by Tikhon Jelvis to do you feel that golang is ugly?, so have a look at that once you’ve read this sec­tion.

A cul­ture of “back­wards com­pat­ib­ility at all costs”

In Go, you im­port pack­ages by URL. If the URL points to, say, Git­Hub, then go get down­loads HEAD of master and uses that. There is no way to spe­cify a ver­sion, un­less you have sep­arate URLs for each ver­sion of your lib­rary.

This is just in­sane.

Go has a very strong cul­ture of back­wards com­pat­ib­il­ity, which I think is largely due to this. Even if you have a flaw in the API of your lib­rary, you can’t ac­tu­ally fix it be­cause that would break all of your re­verse-de­pend­en­cies, un­less they do vendoring, or pin to a spe­cific com­mit.

Coming from the Haskell world, where the at­ti­tude is far more to­wards cor­rect­ness than com­pat­ib­il­ity, this was prob­ably the biggest cul­ture shock for me. Things break back­wards com­pat­ib­ility in Haskell, and the users just up­date their code be­cause they know the lib­rary au­thor did it for a reason. In Go, it just doesn’t happen at all.

The type system is really weak

A common mantra in Haskell is “make il­legal states un­rep­res­ent­able,” which is great. If you’ve never come across it be­fore it means to choose your types such that an il­legal value is a static error. Want to avoid nulls? Use an op­tion type. Want to en­sure a list has at least one ele­ment? Use a nonempty list type. Use proper enums, not just ints. etc etc

In Go you just can’t do that, the type system isn’t strong enough. So a lot of things which are (or can be) a com­pile-­time error in Haskell are a runtime error in Go, which is just worse.

Let’s pick on some spe­cif­ics:

  • No gen­erics

    Want to write a tree where every ele­ment is stat­ic­ally guar­an­teed to be the same type? Well, have fun im­ple­menting a “uinttree”, an “int­tree”, a “stringtree”, and so on. You can’t just im­ple­ment a gen­eric tree.

    But Go does have gen­er­ics, for the built-in types. Ar­rays, chan­nels, maps, and slices all have gen­eric type para­met­ers. So it seems that the Go de­velopers want gen­er­ics, but they don’t want to bother im­ple­menting it prop­erly, so it re­mains a spe­cial case for a few things in the com­piler.

  • No sum types

    The way in Go to handle pos­sibly-­failing func­tions is to have mul­tiple re­turn val­ues: an ac­tual res­ult, and an er­ror. If the error is nil, then the ac­tual result is sens­ible; oth­er­wise the ac­tual result is mean­ing­less.

    This means you can forget to check the error and use a bogus result and, be­cause there are no com­piler warn­ings (an­other wtf), you will know nothing of this until things fail at runtime.

    With a sum type, like Either error result, that just can’t hap­pen.

  • No sep­ar­a­tion of pure code from ef­fectful code

    It is very nice to know, just by looking at the type of a func­tion, that it cannot per­form any side-ef­fects. Go’s type system doesn’t do that.

The tooling is bad

Haskell gets a lot of cri­ti­cism for bad tool­ing, but I think it’s worlds ahead of Go in some cases.

  • Godoc makes it really dif­fi­cult to write good doc­u­ment­a­tion

    Godoc groups bind­ings by type, and then sorts al­pha­bet­ic­ally. Code is not written like that, code is written with re­lated func­tions in prox­imity to each other. The source order is al­most al­ways better than how godoc sorts things.

    Also, godoc doesn’t even sup­port lists:

    Pre­vious pro­posals sim­ilar to this have been re­jected on grounds that it’s a slip­pery slope from this to Mark­down or worse.

    I think that com­ment is par­tic­u­larly dis­cour­aging. Be­cause the de­velopers don’t like Mark­down (and sim­ilar lan­guages), they re­fuse to add even the most basic of format­ting to godoc.

  • There is nothing like GHC’s heap pro­filing

    Go has a snap­shot-­based memory pro­filer. You can take a snap­shot at a point in time, and see which func­tions and types are taking up the heap space. However, there is nothing like this.

    Being able to see not only a snap­shot, but also how things have changed over time, is in­cred­ibly useful for spot­ting memory leaks. If all you have is a snap­shot, all you can really say is “well, the number of al­loc­ated Foos looks a bit high, is that right?” With a graph you can say “the number of al­loc­ated Foos is in­creasing when it shouldn’t be.”

  • There is (was?) nothing like Thread­Scope

    Thread­Scope is a tool for pro­filing per­form­ance of con­cur­rent Haskell pro­grams. It shows which Haskell threads are run­ning on which OS threads, when garbage col­lec­tion hap­pens, and a bunch of other in­form­a­tion.

    If things are slower than ex­pec­ted, it’s great: you can see ex­actly how things are ex­ecut­ing. Go doesn’t cur­rently have any­thing like it, al­though to­wards the end of Dave Cheney’s Seven ways to pro­file Go ap­plic­a­tions talk at GolangUK, he did whip out some­thing which looked rather like Thread­Scope (sadly, a video isn’t up at the time of writ­ing, that I can see).

Zero values are al­most never what you want

Go avoids the issue of un­ini­tial­ised memory by having “zero val­ues”. If you de­clare a vari­able of type int, but don’t give it a value, it gets the value 0. Simple.

Ex­cept that that’s al­most never what you want.

What is a sens­ible de­fault value for a type? Well, it de­pends on what you’re using it for! Some­times there isn’t a sens­ible de­fault, and not ini­tial­ising a value should be an er­ror. You can’t define a zero value for your own types, so you’re kind of stuck.

Zero values caused so many prob­lems over the sum­mer, be­cause everything would ap­pear to be fine, then it sud­denly breaks be­cause the zero value wasn’t sens­ible for its con­text of use. Per­haps it’s an un­re­lated change that causes things to break (like a struct get­ting an extra field).

I would much rather:

  1. Drop the syntax for de­claring a vari­able without giving it a value.
  2. Make it an error to not ini­tialise a struct field.

Lots of boil­er­plate

The cause of the lots of code gen­er­a­tion, I feel.

  • Be­cause you have to check error val­ues, if you want to per­form a se­quence of pos­sibly-er­roring com­pu­ta­tions, where the suc­cessful result of one feeds into the next, there is a lot of typ­ing. In Haskell, you’d just use the Either monad.

  • If you want to sort a slice, be­cause there are no gen­er­ics, you need to wrap the slice in an­other type and im­ple­ment three methods on that type. So that’s four lines of code to sort a slice of uints, four lines to sort a slice of uint8s, four lines to sort a slice of uint16s, and so on. In Haskell, you’d just use the gen­eric sort.

My Overall Feel­ings

I do have one non-Pusher Go pro­ject that I plan to keep de­vel­op­ing, as it’s a fun pro­ject. I picked Go for it be­cause my ini­tial mo­tiv­a­tion was to even­tu­ally in­teg­rate it with what I was doing at work, but that ended up not hap­pen­ing.

Other than that, I will prob­ably never choose to use Go for any­thing ever again, un­less I’m being paid for it. Go is just too dif­ferent to how I think: when I ap­proach a pro­gram­ming prob­lem, I first think about the types and ab­strac­tions that will be use­ful; I think about stat­ic­ally en­for­cing be­ha­vi­our; and I don’t worry about the cost of in­ter­me­diary data struc­tures, be­cause that price is al­most never paid in full.