Abhinav Gupta | About

Go Gotcha: Struct pointers as map keys

A mistake that is easy to make with Go maps is unintentional use of pointers as map keys. For example, consider a map originally keyed on strings.

type UserID int

userIDs := make(map[string]UserID)  // first name -> user ID

Requirements change and it must now use a combination of two strings as the key. This is commonly accomplished by introducing a struct that holds the two strings and using that as the key.

type Name struct {
    FirstName string
    LastName  string
}

userIDs := make(map[*Name]UserID)  // name -> user ID

Problem

The map uses pointers to struct values for its keys.

map[*Name]UserID

This will not yield the expected behavior.

userIDs[&Name{"Jack", "Sparrow"}] = UserID(42)
fmt.Println(userIDs[&Name{"Jack", "Sparrow"}])
// Output: 0

The expectation would be for the example to print 42 but it will print 0.

Why

The map uses pointers for its keys. The two &Name{..} expressions are pointers to different memory addresses.

Suppose the first one has the address 0x000123.

                          0x000123
                          +--------+-----------+
        .---------------->| "Jack" | "Sparrow" |
        |                 +--------+-----------+
        |
userIDs[&Name{"Jack", "Sparrow"}] = UserID(42)

The map will use the numeric value, 0x000123 as the key.

userIDs
+------+----------+-----+
| .... | 0x000123 | ... |
+------+-----|----+-----+
             |
             |     +----+
             '---->| 42 |
                   +----+

The second &Name{..} expression has a different memory address.

                          0x000456
                          +--------+-----------+
                    .---->| "Jack" | "Sparrow" |
                    |     +--------+-----------+
                    |
                    |
fmt.Println(userIDs[&Name{"Jack", "Sparrow"}])

The map does not recognize this numeric value so it will return the zero-value of UserID.

Solution

To match on the values of the fields of the Name struct, use the struct by value.

-make(map[*Name]UserID)
+make(map[Name]UserID)

This behaves as expected.

userIDs[Name{"Jack", "Sparrow"}] = UserID(42)
fmt.Println(userIDs[Name{"Jack", "Sparrow"}])
// Output: 42

This is true for use of sync.Map as well.

var userIDs sync.Map
userIDs.Store(Name{"Jack", "Sparrow"}, UserID(42))
v, _ := userIDs.Load(Name{"Jack", "Sparrow"})
fmt.Println(v)
// Output: 42

Caveat

Note that this advice does not apply for all uses of maps with composite keys. The problem here was unintentional use of pointers as map keys. There are valid cases where use of pointers as map keys is desirable and intentional. Examples of those are out of scope for now.

Written on 2020-03-12.