First thoughts on Go

05 Oct 2012

A more accurate title for this would be First Thoughts Since Having Finally Started Learning Go. My actual first thoughts on Go happened in 2009, and they could be summed up as "Why bother?" Suffice it to say that Google's messaging and positioning when announcing the language left a lot to be desired.

Luckily, around a year later, I started hearing a new round of buzz about Go, this time talking about how great the concurrency features were, how good the language "felt" in general, and how much fun people were having. I took another look and Go was a lot more compelling once there was information to be had other than talk about how fast it compiled.

In the roughly two years since, I've kept up with goings-on in the language's development and community, but never took the time to properly learn Go. That changed about three weeks ago when I decided to use Go for a project and jumped in feet-first.

This article relates my thoughts in those three weeks. This article is not an introduction to Go. If you haven't read the standard introductory materials, little of this is likely to make any sense to you.

Where I'm coming from

To give you some context on my assessment of the language, I've spent nearly my entire career in Linux, writing Perl or Python. I've written trivial programs in C and C++. I've written some pretty decent chunks of Emacs Lisp. And I've written enough shell to learn that the faster I quit writing shell and fail over to a general-purpose programming language, the better off I am.

I've been a programmer all my professional life, mostly doing systems glue, toolsmithing, and backend/support type stuff. I write a lot of programs which are used by other programs, or by programmers. Only very rarely have I written "applications", but I've also never written kernel drivers or realtime systems, etc.

The most important thing

Without a doubt, the single most important step in me getting a good grip on Go has been building up a thorough understanding of the type system.

Interfaces were the most alien part of this, but the type system as a whole is bound up in questions like "what makes Go not C?", "what makes Go not Python?", and "why do people keep saying Go is a 'systems' language, anyway?"

More than goroutines; more than the objectless "object" system; more than the blending of C and Python; more than the choice to standardize the interaction of 3rd-party modules by convention instead of configuration; more than any of these, the type system is what makes Go be Go.

Interfaces

The standard docs will tell you all about what an interface is and how (or, more correctly, some ways) to use them. But it wasn't until I was sitting down and writing Go code that I figured out what interfaces do for the language.

The first thing is that interfaces (the type) power the zero-coupling magic that is interfaces (the struct-of-method-signatures-as-API concept). This is made explicit in the standard intro docs, but it's still a nice little jolt when you start to really understand what it does for you.

Secondly, and much less explicity, interfaces -- specifically the empty interface -- are a side-door to let you get data of uncertain structure (a lot of places where you're consuming JSON make good examples) past the compiler.

The important thing to understand here is that this is not an end-run around the type system. I mean exactly what I said: it gets you past the compiler with no errors, and then no further. To actually use any data imported via empty interfaces, you have to do type assertions all the way down the structure you pulled in -- and these must agree with the data itself.

You still must satisfy the type system, but empty interfaces give you the option to do it at run-time (assisted by Go's introspection capabilities, if you wish) instead of at compile-time.

To make a terrible analogy, it's like sneaking a trusted friend past the bouncer of a private bar, because you know the owner, who is a tough guy with a good heart. Once you're inside, the first thing you have to do is go to the owner and vouch for your friend. The owner can still throw you both out if your friend doesn't pass muster, but at least this way you have a chance.

The importance of this leads me to my next topic.

What's all this about systems languages?

Go is frequently described as a systems language. I've seen people ask repeatedly what that's supposed to mean. I'd never really thought about it before; I "knew" that, for instance, C was a systems language (because Unix is written in it) and, say, Perl wasn't (because no OS is written in it).

This is a very de facto sort of definition, though. Actually, it's less de facto and more entirely circumstantial.

Learning Go has led me to a belief that Go is not a "systems language" because of its grammar, or what can be done with it, or because it is compiled. I believe it deserves the name because of the philosophy of its -- can you guess what I'm about to say? -- data model.

Go is strict about data, except in the one (strict) way in which it is not -- which is to say that Go is strict about data.

Perl is not strict about data. Even with strictures turned on, I can assign a value to an arbitrary element of a nonexistant list, which is itself an element of a (nonexistant) hash, which is inside another hash, inside a list, inside a list, inside a hash... and so on and so forth, as large and complicated as I please. No matter what structure I describe, so long as it is valid, when I assign a value to an element at the bottom of it, Perl will vivify the entirety of the structure for me.

The uninitiated believe that this is because Perl is sloppy or badly designed, or used by the mentally deranged, but the truth is that it's because Perl is designed to believe the programmer knows what they want, and that it is Perl's job to support the programmer so long as Perl cannot prove that the programmer is in error.

This is why Perl is not considered a systems language.

Python is a stricter. I can declare dicts and lists on the fly, and shove them inside each other willy-nilly, but it balks at autovivification of nested structures. "Why would you want to do this?" say the answers on web forums when people ask how to do this. "What you really want is an object! (or a namedtuple, or...)" continue the answers.

Python is more academic and stricter than Perl, but it is not truly strict. This is why it is not considered a systems language.

Go is strict. It demands that you know exactly what your data looks like, and it demands that it be told exactly what your data looks like, so that it can double-check before you are allowed to use anything. All inconsistencies are fatal errors in Go.

However!

I love both Perl and Python, because they let me solve my problems. Most of my problems are defined not by published or agreed-upon requirements, but by data which I have been handed. Perl and Python (and languages like them) make it easy to write programs which are driven by their data, and which exist in order to marshal, transform, or otherwise manipulate that data.

I think the classical systems language assumes that the programmer has been set a task, and that programs exist in order to perform that task and drive the system toward goals. Data is something which is managed in order to achieve those goals, and the data should conform to agreed-upon norms.

Nonconformance, in systems languages, is a fatal error. Conformance, in the world of dynamic languages, and the web, and having to take what you can get, is almost an unexpected pleasantry.

But it's important to realize that most systems languages are old. Very old. C dates to 1972. C++ dates to 1983. Java dates to 1995. Depressing as it is to say aloud, 1995 was seventeen years ago. That's six years longer than the gap between C and C++ and five years longer than the gap between C++ and Java.

Seventeen years is a huge gulf of time in computing. It's huge in terms of hardware resources, in terms of programming language research, and in terms of what people think of as reasonable for a language runtime to provide as support to programmers.

Go is a systems language, born of the modern world. Go lets me solve my problems in a manner which is not foreign to me.

Who cares about that anyway?

People like me probably will. Data mungers. Systems administrators. People who need a language to let them do their work, no matter how messy the task they've been handed, without punishing them for having to solve a certain kind of problem.

I'm really glad that Go supports this. I don't view it as a compromise or a failure to protect the purity of essence of the language; I view it as an informed design choice to provide a way for people to handle real-world problems.

A language which is compiled, garbage-collected, has baked-in concurrency features, and doesn't make me want to paint the walls with my own entrails in dying protest against the cruelty and/or pomposity and/or myopia of the designers? Yes, please.

All this leads to:

Stealing from Python is great

I don't mean the lack of (explicit) semicolons. I mean this:

for key, value := range somemap {

Mmmm, data-driven logic. Stealing the range keyword is one of my favorite design decisions in the language, but as you know by now, I'm biased.

And speaking of maps (though range works over many things other than maps)...

Maps aren't quite hashes/dicts/objs

Unlike Perl's hashes, Python's dicts, and Javascripts objects, Go's maps are not a composite datatype. In fact, both the key and the value of the map are typed.

map[string]int

That declares a map with string keys and integer values (a string-to-int map). Every key must be a string, and every value must be an int.

You can have maps which have maps as values, so they can be nested. That's perfectly legal:

map[string]map[string]map[string]int

That declares a map-of-maps-of-maps-of-ints. But at the bottom, all I can store is integers. I can't store a mixed bag of data down there. If I'm modelling hardware in a datacenter, maybe my top-level map is representing racks, the middle map is representing machines, and the bottom map is where I want to store all the information about individual machines. As a Perl/Python programmer I look at the fact that map values can only be a single type, and I despair. Am I going to have to store everything as strings and write getter/setters to handle translation to other datatypes? That would be horrible!

No. I just need to realize that maps are for mapping and not for storing composite data. Then I need to remember that there's a whole other type which is for that: structs. What I want to do, in Go, is define a struct type which models machines, and then build a map-of-maps-of-my-machine-struct. I don't bottom out with another hash/dict. I bottom out with a (pointer to a) struct.

type Machine struct {
    // model a machine here
}
map[string]map[string]*Machine // probably want the values to be pointers

This is, I'm sure, bloody obvious if you're been writing C for years, but it took a couple minutes of thinking for me.

Understanding types make the errors better

Calling Go's error messages "terse" is being polite. They are the sort of errors that only the compiler author could love. The only good thing I can say about them is that getting a handle on the language's type system makes most of them much more comprehensible.

This may be a localized phenomenon though, because most of my errors tend to involve doing things that the type system doesn't like.

In any event, grokking the type system makes the errors easier to understand because the type system constrains the sorts of runtime errors you can have. At the same time, it also narrows down the number of things a vague error message could possibly be complaining about.

I'd still rather have completely awesome diagnostics, like those provided by Perl 5.10+ or Clang, but I'll take what help I can get.

I'm nervous about databases

There are no database drivers in the standard library, only a standard API for any given database driver.

To be fair, this is exactly the situation one finds with Perl, and Python only improves upon it by having sqlite baked in. But time has given evolution time to work in the Perl and Python ecosystems, where there is functionally one well-known, community-approved driver module for any given DBMS.

So consider this not a complaint, but a statement of unease about the youth of the Go ecosystem. Just because it's unavoidable doesn't mean I have to like it.

You can use it as a scripting language

Remember how I griped that that Google's initial messaging on Go was (basically) that it sure did compile fast? This finally became useful (to me) with the release of Go v1, when tooling officially moved from using makefiles (or manually running the compile->link->execute cycle) to use of the go tool to handle compiles, installs, dependencies, etc.

One of the things go added was the run command, which does a one-shot compile-link-execute of a source file. This, combined with the speed of the compiler, means that you can effectively use Go as a scripting language. Or at least use a highly-stepwise development style, as I tend to do with scripting languages.

I think this is great, and a lot of fun.

Closing thoughts

I don't have a "conclusion" or any such thing. I'm just getting started with the language.

I spent two years admiring the language from afar, and learning about it but not actually learning it. I'd absorbed a good deal of theory, and stood around on the periphery of the language's community. I knew I liked Go. I just didn't know if it would be as suitable in practice as I thought it would be, or if its differences in design would make it unexpectedly onerous to get a grip on once I started trying to use it in the real world.

But taking a couple days to work through the Go Tour and read the spec and Effective Go, followed by jumping in and starting to work on a real problem has resulted in a pretty quick ramping-up for me.

This document covers, in a broad way, the things that I had to work out for myself. Hopefully it can fill in those gaps for other people. Until next time.