On the Language Design of Golang

Presented on November 30, 2017
Presenter: Mika

Preview

Mika presented multiple accounts of how golang’s design betrays itself. The documentation behind this covers many people’s writings, summarized here in a nice way. The presentation was divided in two: one was a quiz, which outlines most of golang’s features, and also shows why and how it’s stuck in the 70s by choice and design. The other part is a couple of weird constructions which demonstrate how golang’s features can be used outside their stated semantics, and result in unsafe or right-out weird behaviours.

Summary

To introduce the lab to the language, we went through a comparison of golang and another language, which is, for the purpose of presentation, hidden behind a cool name (Brand X). They have almost equal feature sets, including golang’s most prominent and novel ones, like channels and goroutines. At the end of the whole comparison, going feature by feature, we discover that the language in question, one with features seeming a little bit antique but consistent, mostly reasonable and stable, is Algol-68. There were many guesses along the way and it was fun.

After that, we went through some examples, exemplifying several golang moments of disbelief. The best part was showing that nil isn’t a keyword, and that builtin type values, true and false are not reserved. Here’s a thing that is actual proper golang:

var nil = 3
var true = false
println(nil)
println(true == false)

Several more things reveal themselves as antipatterns, including the exception of errors in the language proper, instead opting for panicking and a automagical multi-return function mechanism:

retval, err := func(param)
if err != nil {
    // handle error
}

Even though this is a “pattern” in te language, it builds boilerplate on top of boilerplate, and this shows: for starters, it’s a fact that the three most used words in golang are “if”, “nil” and “err”. If we don’t handle the error or even check for it, and have used the multi-return mechanism, the errors are silent. Thus, type conversions can be done in an unsafe manner:

a, err := func(param) // expected to return float64, for example
b, err  = a.(int64)
c, err  = b.(float64)

After the fact, c is 0. The data created by func(param) is lost.

Another fun fact is nil equivalence: nil is untyped, and it should be equal to nil passed to empty interfaces or functions. However, even though the first two statements below are true, the third is, mysteriously, not.

interface{}(nil) == nil			// true
(func())(nil) == nil			// true
interface{}(nil) == (func())(nil)	// false! compile-time error

The reason behind this is not what we expected: “func doesn’t contain method ==” and thus can’t compare. This, however, makes no sense, as the second statement compiled. The investigation of this phenomenon led us to the specification of interfaces: in golang, interfaces are defined as a tuple of (type, value). The only accepted nil value for an interface is (nil, nil). If we go from another type to nil via conversions (as described above), the result is always of kind (T, nil), where T is not nil. This seems like a very bad design choice that can only lead to catastrophy. All of this, for what? Rob Pike explains:

The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.

Discussion

This discussion was an interactive one (lots of fast questions and tests), and is hard to make a write-up of. Golang is a bad language for prototyping, as it doesn’t let you compile if you have any unused imports or variables. This way, we had more “nil is declared but unused” errors than type-system hardships that we expected. If nothing else, at least the compiler is really fast, so going through these things was fast.

Several other examples outlined the idiomatic use of unsafe type conversions: the (anti-)pattern of using multiple return values to get both a value and an error, instead of a sum type or something similar. Mixing this pattern with default undefined values and variable shadowing can easily get us to a place where data is lost and yet the errors that caused the loss aren’t reported. We went through trying things together and had lots of fun.

Edit: William found this after the discussion, so I guess people are still reeling.