Go: a nice language with an annoying personality

2013-01-18

Last week, I had the pleasure of attending Dropbox's annual company hack fest. It was a great opportunity to get a look at how Dropbox works internally, and mingle with the smart and driven folks who make one of my favourite products. In the spirit of hack week, me and my friend @alexdong decided to do our project in Go. We'd both wanted to explore the language, but had never quite been able to make time - a week-long code holiday seemed to be the perfect opportunity. I was hopeful that Go would turn out to hit a magical sweet spot: a light set of abstractions hugging close to the machine, while still providing the indoor plumbing and civilized conveniences of life that I had grown used to with languages like Python. Five days of furious hacking later, I can report that Go might well deliver on this promise, but has enough annoying personality quirks that I will think twice about basing any more projects on it.

My main beef with Go has nothing to do with fundamental language design, and may seem almost inconsequential at first glance. The Go compiler treats unused module imports and declared variables as compile errors. This is great in theory and is something you might well want to enforce before code can be committed, but during the actual process of producing code it's nothing but an irksome, unnecessary pain in the ass. Let's look at a concrete example, starting with a snippet of code as follows 1

import (
    "io/ioutil"
)
...
...
    m, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
...
...
    DoSomething(m)

I'm a firm believer that printing stuff to screen is a programmer's best debugging tool, so say we're hacking away and want to print the value of m while running our unit tests. We change the code as follows, adding an import for the "fmt" module and a call to Print:

import (
    "io/ioutil"
    "fmt"
)
...
...
    m, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
    fmt.Print(m)
...
...
    DoSomething(m)

Now we keep hacking, and want to comment out the print statement for a moment like so:

import (
    "io/ioutil"
    "fmt"
)
...
...
    m, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
    //fmt.Print(m)
...
...
    DoSomething(m)

This is a compile error. We have to switch contexts, move to the top of the module, also comment out the import, and then move back to the spot we're really hacking on:

import (
    "io/ioutil"
    //"fmt"
)
...
...
    m, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
    //fmt.Print(m)
...
...
    DoSomething(m)

A few seconds later, we want to re-enable the Print statement - so up we go again to the top of the module to re-enable the import. This is even worse when we want to, say, comment out the DoSomething call while hacking:

import (
    "io/ioutil"
)
...
...
    m, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
...
...
    //DoSomething(m)

This is also a compile error because now m is unused. We have to hunt up in our code to find the declaration, which could be explicit or implicit using an := assignment. So, in this case we find the declaration, and use the magic underscore name to throw the offending value away:

import (
    "io/ioutil"
)
...
...
    _, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
...
...
    //DoSomething(m)

That should fix it, right? Well, no. It turns out we've previously declared and used err (a very common idiom), so this is still a compile error. We're using the "declare and assign" syntax, but have no new variables on the left-hand side of the ":=". So we need to make another tweak:

import (
    "io/ioutil"
)
...
...
    _, err = ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
...
...
    //DoSomething(m)

Five seconds later, we want to re-enable DoSomething, and now we have to unwind the entire process.

The cumulative effect of all this is like trying to write code while someone next to you randomly knocks your hands off the keyboard every few seconds. It's a pointlessly pedantic approach that adds constant friction to your write-compile-test cycle, breaks your flow, and just generally makes life a little harder for very little benefit. There's no way to turn this mis-feature off, no flag we can pass to the compiler to temporarily make this a warning rather than an error while hacking2.

The irony of the situation is that I agree with the sentiment behind this. I don't want dangling variables or imports in my codebase. And I agree that if something is worth warning about it's worth making it an error. The mistake is to confuse the state we want at the conclusion of a unit of hacking3, with what we need at every point in between, during the write-compile-test cycle. This cycle is the core of the process of actually producing code, and the exhilarating sense of weightlessness that you get when hacking in Python is largely due to the fact that the language works really, really hard to optimize this process. Go has given away this feeling of exhilaration, basically for nothing.

Despite all this, it's still possible that the benefits of Go do outweigh its irritating personality. Interfaces, memory management, first-class concurrency and static type checking is a knockout combination, and the language in general has something of the taut practicality that I love in C. So, despite the rantiness of this post, I'll keep hacking on our project and make sure I produce a few thousand more lines of code before making a final call on the language. Look for a project release and a blog post along these lines in the coming months.

1

Ellipses indicate "an arbitrary amount of intervening code"

2

I edited this paragraph a bit for tone. I originally accused the Go documentation of being faintly smug about all of this - which is not fair, and doesn't add anything to the argument.

3

Why don't we have a word for this? By "unit of hacking", I mean the work that goes on between starting to hack on a change-set and doing a commit. At the beginning and at the end, the code is in a clean state, but in between there are many periods of transition where cleanliness requirements are relaxed.