░░twurst.com / list-map-key (2017-09-24)

Linked Lists As Map Keys – A Weird Go Trick

Sometimes the simplest things can be surprising. In this installment, we’ll explore Go value equality and a subtlety regarding non-pointer interface values. A hat tip to Nick Johnson, who told me of this trick when discussing his smart EVM disassembler “evmdis”.

Let’s say you’re iterating a hierarchy in Go and you want to track whether a certain path of arbitrary length has been visited. The easiest way is to store all visited paths in a map. This is straightforward if your path is a string, but what if the elements of your path are structured objects? Turns out you can still use a map!

Here’s an example. This is the path element:

type elem struct {
    name  string
    index int
}

Go slices can’t be used as map keys. So you can’t do this:

type path []elem

seen := make(map[path]bool) // Error: invalid map key type path
seen[path{{"foo", 1}, {"bar", 2}}] = true

But you can do this:

type path interface{} // elem | nil

type elem struct {
    name  string
    index int
    link  path
}

seen := make(map[path]bool)
path1 := elem{"foo", 1, elem{"bar", 2, nil}}
path2 := elem{"foo", 1, elem{"bar", 99, nil}}

seen[path1] = true
fmt.Println("seen path1? →", seen[path1])
fmt.Println("seen path2? →", seen[path2])
seen path1? → true
seen path2? → false

What’s happening there?

Map keys must be comparable and use the same rules as the equality operator. Intuitively, you can think of Go comparisons as being shallow, not deep. Go values are equal when their content is equal. References are not followed. But there’s a catch for interfaces, which are a kind of reference:

Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

When comparing the elem struct, all fields including link are compared. In our case, the dynamic value of link has type elem or nil – recursion! The equality operator traverses the whole list, checking for deep equality.

Should I use this in production code?

There is a huge downside to this trick: the struct’s fields must be comparable. There is no way to declare your intent of comparability to the compiler. If the element type were modified by someone who is unaware of the subtlety, you might get a nice crash or worse, a false positive.

So no, don’t use this in production code. But it might come in handy some day.