Go is Weird

Go is weird. For all its intended (and frequently achieved) simplicity and straightforwardness, I keep being surprised by its rough edges and seemingly arbitrary corner cases. Here is one.

Can you spot what’s wrong with this code?

package main

type Val struct {
    Name string
    Count int
}

func main() {
    data := make( map[string]Val )

    // populate data ...

    for key, _ := range data {
        data[key].Count += 1
    }
}

We create a map of Val types, where each Val struct contains a Name and a Count. We then iterate over the elements of the map, incrementing the Count member for each element in the map.

What could possibly be wrong?

Answer: It won’t compile!

go build weird.go
./weird.go:15:3: cannot assign to struct field data[key].Count in map

You see, Go prohibits you from updating an element in a map. This is a design feature of Go, not a bug: map elements are not addressable. The rationale behind this is not obvious, I have found arguments relating it to the locking/synchronization behavior of maps, and to questions of memory consistency (what if a map element is deleted, after its address has been taken?).

My main point is a different one: who, looking at the code snippet above, would expect this kind of behavior? Nobody, I am sure. (In particular since, even knowing of this behavior, it is not obvious to see the technical reason or rationale!)

For a language that was so explicitly designed for simplicity and consistency, these little, seemingly arbitrary exceptions and unpredictable inconsistencies in Go strike me as very, very odd.

In any case, this issue apparently keeps tripping people up; a quick Google search on cannot assign to struct field in map yields multiple posts on StackOverflow and elsewhere.

Obviously, there are trivial workarounds. The first one uses a temporary variable:

func main() {
    data := make( map[string]*Val )

    // populate data ...

    for key, _ := range data {
        tmp := data[key]
        tmp.Count += 1
        data[key] = tmp
    }
}

The second stores a pointer to the struct in the map instead:

func main() {
    data := make( map[string]*Val )

    // populate data ...

    for key, _ := range data {
        data[key].Count += 1
    }
}

It is amusing (or maybe not) to see that this very issue has been debated, on and off, for more than 10 years, on the offical Go discussion page: the first entry dates from 2012, the last one (as of this writing), from 2023. The discussion itself is interesting, because it highlights some of the implications and edge cases touched by this issue. (The upshot so far is that the compiler now generates a better error message if it detects this condition.)

More information on addressability in Go has been collected here and discussed in the context of structs inside of maps here.

I have not thought about this deeply, but I wonder whether Go’s lack of an over-arching design paradigm (along the lines of “everything is an object”, “a file is a bag of bytes”, etc) is responsible for these sudden, admittedly minor, but irritating inconsistencies.