Non-uniform Language Design in Go
One of the very first things I noticed (https://goo.gl/xnWDPs
) about Go is that the language is inconsistent
. By that I mean that there is a clear difference between what the builtin functions can do and what I as a language user can do.
Take the "comma ok" idiom presented in the Effective Go document (https://golang.org/doc/effective_go.html#maps
). Basically, both of these lines compile:
x := someMap[key]
x, ok := someMap[key]
In both cases, the value from someMap[key] is put into x. If key is not present in someMap, a zero value is assigned to x, and in the second line ok will be false.
So far so good — looks like a handy syntax. The same pattern can be used for type conversion where you have an interface value from which you want to extract a concrete value of a given type:
x := someValue.(*ConcreteType)
x, ok := someValue.(*ConcreteType)
The syntax itself doesn't follow any obvious pattern here: it seems we're calling a method with an empty name, passing a type as argument?!
Anyway, the first line succeeds when someValue has ConcreteType, otherwise you get a runtime panic. The second line never panics, instead you get a zero value for x (nil) and ok is false if someValue was not of type ConcreteType.
Since this is presented as an idiom, I expected to take advantage of it with my own functions. After all, Go has support for multiple return values... However, it turns out that the so-called idiom only applies to the two cases above! I cannot write a function getElement(key string) (int, bool) and call it like this
n := getElement("foo")
n, ok := getElement("foo")
I'm obliged to use the "blank identifier" instead. Why does that not apply to the builtin syntax? Probably because it would make it annoying to use maps and interface conversions! :-) This means that you cannot use my getElement function as flexible as a normal map, e.g., this doesn't work:
getElement("foo") + getElement("bar")
This preferred treatment of the builtin types is annoying. In particularly when there are many languages that avoids this. Python is one example where everything in the language works "without cheating". The builtin dict (map) type behaves as if
it had been implemented using a normal Python class that defines the ＿getitem＿ method.
This is important for at least two reasons. First, it makes it easy to understand the builtin types since you could in principle have implemented it yourself. No cheating or special treatment going on.
Second, it makes the language flexible. People can extend the builtin types and add entirely new types. Do you need a bounded queue? No problem, implement it and you can give it an interface that makes it look like a builtin list. With duck-typing, you can pass your queue into a function that doesn't know about it — provided the function only requires list-like functionality.
Ironically, Go has the same kind of structural typing, but because the builtin types don't adhere to any interface, you cannot make your own slice-like or map-like types and expect to iterate over them using the builtin range. That seems like a lost opportunity, something that Java managed to do better with the Iterable interface and its enhanced for loop syntax that works for arrays too. In Java, the builtin array type is treated as a special case of a more general syntax there — instead of being the only
case as in Go.
Part of the charme with Go is it's simplicity, but I feel like it should be possible to make a simple language that avoids these special cases.