While waiting for a build pipeline to finish I was reading a blog post about Go maps.

To summarize, map types are reference types, like pointers or slices, and so their zero value is nil and their length is 0. However, if you try to assign to an uninitialized map you get a runtime panic:

var m map[string]int
fmt.Println(m == nil) // true
fmt.Println(len(m))   // 0
m["route"] = 66       // panic: assignment to entry in nil map

One way to initialize a map is using the built in make function:

m = make(map[string]int)
m["route"] = 66

Another way would be to use a map literal:

m = map[string]int{
    "route": 66,
}

To retrieve the value stored under the key route we do:

v := m["route"] // v == 66

If the key does not exist in the map we get the value type's zero value:

v = m["root"] // v == 0

But now we might be unsure whether 0 is a "real" value stored in the map under the key root or the type's zero value. The solve these doubts we use the two-value assignment:

v, ok := m["route"] // v == 66, ok == true
v, ok = m["root"]   // v == 0, ok == false

The first value (v) is assigned the value stored under the given key or a zero value if the key does not exist. The second value (ok) is a bool that is true if the key exists in the map, and false if not.

However, it's sometimes useful to rely on the zero value returned from a map when the key is not present. Two examples showing this caught my attention and I re-implemented them and wrote about them to understand them better.

Booleans

The first example shows how to exploit a boolean type zero value - false. The example traverses a linked list of nodes and prints their values. Linked list is a group of elements of certain type (which is Node in our case) where each element can point to another element. Since we are ranging over all the elements we want to detect cycles that would throw us into infinite loop.

First we define the Node type:

type Node struct {
	Next  *Node
	Value any
}

Then we build a small linked list that can be visualized like this:

 n1                      n2                      n3
+----------+---------+  +----------+---------+  +----------+-----------+
| Value: 1 | Next: *-|->| Value: 2 | Next: *-|->| Value: 3 | Next: nil |
+----------+---------+  +----------+---------+  +----------+-----------+
n1 := &Node{Value: 1}
n2 := &Node{Value: 2}
n3 := &Node{Value: 3}
n1.Next = n2
n2.Next = n3

We'll use visited variable that maps Nodes to booleans effectively telling us whether we have already visited the given node:

visited := make(map[*Node]bool)
for n := n1; n != nil; n = n.Next {
    if visited[n] {
        fmt.Println("cycle detected")
        break
    }
    visited[n] = true
    fmt.Println(n.Value)
}

When we run the code we get:

$ go run ./booleans/main.go 
1
2
3

All is good. Now let's create a cycle that looks like this:

 +-----------------------------------------+
 |                                         |
 V                                         |
 n1                      n2                |     n3
+----------+---------+  +----------+-------|-+  +----------+-----------+
| Value: 1 | Next: *-|->| Value: 2 | Next: * |  | Value: 3 | Next: nil |
+----------+---------+  +----------+---------+  +----------+-----------+
n2.Next = n1

And we verify that our code detects this cycle:

$ go run ./booleans/main.go 
1
2
cycle detected

Slices

Second example is about a map of slices. First we create a slice of people and their likes:

type Person struct {
	Name  string
	Likes []string
}

var people = []*Person{
	{"Frodo", []string{"mushrooms", "pipes"}},
	{"Sam", []string{"mushrooms", "beer", "pipes"}},
	{"Gandalf", []string{"pipes"}},
}

Next, we range through these people and map the likes to persons:

likes := make(map[string][]*Person)

for _, p := range people {
    for _, l := range p.Likes {
        // Appending to a nil slice just allocates a new slice; 
        // there's no need to check if the key exists.
        likes[l] = append(likes[l], p)
    }
}

Now, we can ask various questions about people's likes, for example:

for _, p := range likes["Sauron"] {
    fmt.Println(p.Name, "likes Sauron.")
}

fmt.Println(len(likes["Sauron"]), "people like Sauron.")

Note that since both range and len treat a nil slice as a zero-length slice, the last two examples work even if no person likes Sauron.

Reply

or to participate