Bryan Klimt
Go

I’ve been learning Go the last few days. I’ve been trying to learn the rules of the language, so that when I encounter new code, I can apply those principles and understand it quickly and thoroughly. But I’ve been having a really hard time. I get the feeling that Go was designed by System Engineers, who focus on implementation. The API Designer in me is getting frustrated with the lack of consistency in the language. There are more exceptions than rules. Here are some of the things I find the most confusing.

Instantiation

Let’s say you want to create a new instance of Foo. How do you do it? Well, first, you’ve got to know what primitive type Foo is descended from. If it happens to be an int or bool or something like that, you can do either of these:

var x Foo
y := 3

If Foo is derived from map, slice, or chan, then you use this special function called make:

hash := make(map[string]int)
slice := make([]int 100)
c := make(chan int)

Why is there this special function that construct these three random types? I don’t know.

If Foo is derived from struct, then you have to know whether the struct is usable when it’s zero-valued. I think most of them are. But watch out, because there’s nothing to stop you from making one that’s zero-valued when it’s not valid.

There’s a function called new that can create a zero-valued struct, but as far I can tell, you’re never supposed to use it, because you can use the literal syntax instead:

// Foo is valid when zero-valued. These two declarations are equivalent.
fooPtr := &Foo{}
fooPtr := new(Foo)

Why does new exist?

If Foo is derived from a struct, and it doesn’t work when zero-valued, then there should be a function somewhere called something like NewFoo. Good luck finding the right one.

foo := foopkg.NewFoo(arg1, arg2)

Value Semantics

Every type in Go uses value semantics, meaning passing it to a function will be a pass-by-value. Except maps and slices. They are passed by reference. I think it’s only those two. Despite the similarity between arrays and slices, the former is a value type, while the latter is a reference type. I think channels are value types, which is strange since it’s the only other type created with make. (Correction: Channels are reference types, so maybe that explains make; it generates the three reference types.)

Partial methods

You can add methods to types outside of the file where the type was defined. This should be incredibly powerful:

type Foo struct {
  bar string
}

// A method on Foo.
func (foo *Foo) setBar(aString string) {
  foo.bar = aString
}

Unfortunately, this feature has been neutered by only being able to extend types in your own package. You can’t add methods to built-in types like string or int. This means those types can never fulfill any new interfaces you add. You can derive a new type from those types, and extend that, but then you have to explicitly cast wherever you use it.

type ExtensibleInt int

func (x ExtensibleInt) timesTwo() int {
  return x * 2
}

var myInt extensibleInt = 3

// You have to explicitly cast it back to int.
fmt.Printf("%d", int(myInt));

Non-standard library

Unfortunately, not being able to add methods to string makes the standard library a bit wonky. For example, you have to write strings.HasPrefix(str, pre) instead of the more obvious str.HasPrefix(pre).

The standard library also seems to be missing a lot of the most common data types. As far as I can tell, there are no standard stack or queue types. But there is heap. How can a language provide an http server, but no stack type?

In-band signaling

Go has a wonderful feature that a method can return multiple values.

// ok is true iff the key is in the map.
value, ok := someMap[key]

As the designers of Go point out, this helps you avoid in-band signaling. By not conflating a special value (such as nil) with the lack of presence, you are free to have nil be a legitimately returned value.

Unfortunately, I don’t think they’ve really internalized this concept. Strings can’t nil, which is fine on its own, But lots of folks on the internet have been asking for a nullable string type, such as how nullable types work in C#, or how String works in Java. That way, you can write functions where a string is optional, but the empty string is also a valid input.

So what’s the response from Go’s designers about the requests for nullable strings? To treat the empty string as a special value. Sometimes it’s like they haven’t even read their own design justifications.

Conclusions

This post has been about my problems with learning Go, but let me be clear that I don’t think Go is a terrible language. It has some really great features, such as multiple return values and the := operator. And the results seem to be very efficient binaries that build almost instantly. It may be an awesome language once you get to know it well.

But all the inconsistencies in the language design have made learning it feel like a chore.

  1. bklimt posted this