2010-10-12

My thoughts on Go

When my thesis work hit a lull point -- I was waiting to hear back from my committee on the latest edits which were eventually cleared -- I decided I wanted to shift gears off of Oplop and doing so much JavaScript work (which had culminated in my blog post on transitioning from jQuery to Google Closure. Me being me, that meant learning a programming language from my laundry list of languages to learn. I decided to learn Go to continue with my Google theme as of late. As is true with all languages that I "learn", I didn't do any deep dive into Go beyond reading what I could (in this case the tutorial, language spec, and Effective Go) and doing some rather basic example code that I do for every language and one example that takes about 100 lines or more and varies almost every time based on what strikes my fancy. In other words I am in no way a Go expert, but I would like to think I have an inkling of what kind of "flavour" Go is in terms of a language.

First and foremost, Go is an opinionated language. This is made obvious by the fact the document Effective Go "gives tips for writing clear, idiomatic Go code". Now for some people, this is a bad thing. Variety is the spice of life for some programmers, and so they want a language that lets you do things in a various ways based on personal taste (e.g., C++, Perl). Others, though, prefer a strong vision of how a language should work for more coherency (e.g., Python). The former approach has the drawback of making it hard to read other people's code, while the latter approach requires that you actually like the approach the language forces upon you. Go definitely falls into the latter camp of an opinionated language.

Consider formatting. Go takes the approach that any global variable in a package that starts with an uppercase letter is exported, while everything else is not. There is no public/private or static keywords to say what is and is not available outside of the package, just whether there is a capital letter. That clearly puts formatting into the realm of semantics.

Then there is gofmt. Go includes a tool that will reformat your source code for you to follow the Go team's style guide. Now you can control things like spaces over tabs (they prefer tabs for some sick reason) or how many spaces a tab represents, but that's kind of it. It means the entire standard library is formatted very consistently which is nice, but it also means that there is One True Way of formatting.

All of this really smacks you in the face when you try to to an if ... else statement. Take the example of:

if x {
    fmt.Println("true!")
} else {
    fmt.Println("false!")
}

Look at the placement of the braces. Now realize that they can't be on any other lines than the one they are on. So you can't do K&R C style where the opening brace is on its own line. And you can't start the else clause on a line separate from the preceding closing brace. While Go might more-or-less follow C-style formatting, it definitely is not as loose as other languages that use the same style when it comes to formatting rules.

One thing I really appreciate in programming languages is consistency/pervasiveness. If a language provides some feature, I appreciate it when it is generalized in such a way that is it consistently used through the language, becoming pervasive throughout. In Python 3, think of how list comprehensions are now defined in terms of generator expressions. Think of how numbers in Python are just some type that anyone could implement (unlike some crappy languages which claim to be fully object-oriented and yet still have the concept of primitives vs. objects; I'm looking at you, Java). When the language plays by rules that you can also use it makes it easy to comprehend how the overall system works since there are no special cases to remember.

C as a systems language does this, but at a price. Take memory allocation on the heap. Calling malloc just gives you raw memory to do whatever you want with. This makes it a consistent story in terms of how to use heap memory, but it also means you can abuse heap memory. I mean it is a little warped that you can take an array and access the third item through arr[2] or *(arr+2).

Go as a systems language sacrifices consistency for safety. For instance, there are two kinds of memory allocators, new and make. The former is for memory which is type safe even when the memory is zero'ed out (think structs). For make, it's used when the underlying data needs to be formatted in a special way (think maps). In C there would not be this distinction, but then again if you screw up and forget to format your map you are going to have your program blow up in your face (or worse). This and other decisions means that to gain safety Go sacrificed consistency in some places. I understand the decision and I don't think there is a good solution around it, but it does sacrifice mental comprehension somewhat which always sucks for a programming language.

I do have to admit I like goroutines. Now I know someone has already begun to say "but Erlang does a more thorough job of handling message passing concurrency thanks to its design to support recoverability from errors, etc. But you know what? Sometimes I just want an easy to way to do factorial in parallel. Goroutines work rather nicely in those situations where something is embarrassingly parallel. And considering this is a systems language, it's a very nice solution.

Being such a new language, the third-party documentation is rather lacking at the moment. That's unfortunate as trying to find solutions to common issues like how to convert a []byte array to string (answer: bytes.NewBuffer(arr).String()) require figuring it out for yourself. Obviously if Go takes off then this issue will eventually rectify itself.

Being a language that is trying to appeal to the pre-existing C user base, Go tries to be innovative in certain areas, but not in others. This ends up making the language feel like it compromised in the name of appeasing C users in certain cases. For instance, the increment & decrement operators (x++ & x--) are in the language, but only in postfix notation. That's fine as C code typically became much harder to follow when prefix increment/decrement came into play as you had to start thinking about side-effects of expressions. But one of the handy things in C was using increment/decrement as expressions to tighten up your code. But in Go increment/decrement are statements, negating the usefulness of them; x += 1 is not exactly that much harder to type compared to x++.

Or take variable declarations. Let's say you have a variable you want to define and give an initial value. You can do that in the following three ways:

var x int
var x = 42
x := 42

OK, so the first two look like JavaScript-like declarations but with type support. The last version is obviously the shortest but doesn't have direct support for type declarations or assuming the default zero value. Why not try to unify around the short variable declaration and remove the different ways to declare a variable?:

x := 42
x int := 42
x int := _

And honestly, is having to specify a default value that big of a thing? If you simply require the declaration assignment then you don't have to come up with some zero value representation that is type-safe for everyone. How hard would it be for people to be forced to simply say x := 0 or x []byte := nil? Generalizes the syntax more and removes more cognitive overhead of having to remember the various ways to declare a variable along with still preventing the use of a variable that has been unassigned!

Considering Go is a systems language, I would want it to be cognitively simple as possible. You already have to do more work than compared to a language like Python anyway, so why not make sure that any extra cognitive work I must do has some real payoff? Like why even bother with having a switch statement if you are not going to go as far as functional languages do with pattern matching? Yes, Go does have a type switch which is nice, but why even support an expression switch? Now I have to remember both kinds of switches plus the syntax which is slightly non-standard for the declaration in order to support both kinds. It's like the Go designers are taking two steps forward by taking out the evil parts of C, but then they take a step back by supporting syntax that is marginally beneficial.

So what are my overall impressions? Whenever I learn a new language I try to compare it only to languages which I would use in the same situations, e.g., I would not compare Python to Java as they fill different niches. The ones I always have in my head are systems, compiled, and dynamic languages (although the "compiled" division is tenuous at best since Python fills that typical role as well thanks to having very good performance, but I have to recognize that sometimes a statically typed, compiled language has its place and C is just too low-level). Up until now if I was asked to list my favourite systems/compiled/dynamic languages list, it would have been C, Scala/OCaml, Python (still looking for a compiled language obviously). But now I would honestly slot Go in over C. Go provides the low-level access and lack of automated hand-holding needed in a systems language, while providing features that C would have provided if developed today (e.g., garbage collection and strong typing). The typical trip-ups I come across when coding in C are dealt with in Go. While I may have wished they simplified the syntax more and gone with a Python style and dropped the damned curly braces, it's a more-or-less minor complaint compared to what one gains in a systems language in using Go.