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.