Lesson 19. The ever-versatile map

After reading lesson 19, you’ll be able to

  • Use maps as collections for unstructured data
  • Declare, access, and iterate over maps
  • Explore some uses of the versatile map type

Maps come in handy when you’re searching for something, and we’re not just talking about Google Maps (www.google.com/mars/). Go provides a map collection with keys that map to values. Whereas arrays and slices are indexed by sequential integers, map keys can be nearly any type.

Note

This collection goes by several different names: dictionaries in Python, hashes in Ruby, and objects in JavaScript. Associative arrays in PHP and tables in Lua serve as both maps and conventional arrays.

Maps are especially useful for unstructured data where the keys are determined while a program is running. Programs written in scripting languages tend to use maps for structured data as well—data where the keys are known ahead of time. Lesson 21 covers Go’s structure type, which is better suited for those cases.

Consider this

Maps associate a key with a value, which is handy for an index. If you know the title of a book, iterating through every book in an array could take some time, just like looking through every shelf of every aisle of a library or bookstore. A map keyed by book title is faster for that purpose.

What are some other situations in which a map from keys to values could be useful?

19.1. Declaring a map

The keys of maps can be nearly any type, unlike arrays and slices, which have sequential integers for keys. You must specify a type for the keys and values in Go. To declare a map with keys of type string and values of type int, the syntax is map[string]int, as shown in figure 19.1.

Figure 19.1. A map with string keys and integer values

The temperature map declared in listing 19.1 contains average temperatures from the Planetary Fact Sheet (nssdc.gsfc.nasa.gov/planetary/factsheet/). You can declare and initialize maps with composite literals, much like other collection types. For each element, specify a key and value of the appropriate type. Use square brackets [] to look up values by key, to assign over existing values, or to add values to the map.

Listing 19.1. Average temperature map: map.go
temperature := map[string]int{
    "Earth": 15,                                          1
    "Mars":  -65,
}

temp := temperature["Earth"]
fmt.Printf("On average the Earth is %v° C.
", temp)      2

temperature["Earth"] = 16                                 3
temperature["Venus"] = 464

fmt.Println(temperature)                                  4

  • 1 Composite literals are key-value pairs for maps.
  • 2 Prints On average the Earth is 15° C.
  • 3 A little climate change
  • 4 Prints map[Venus:464 Earth:16 Mars:-65]

If you access a key that doesn’t exist in the map, the result is the zero value for the type (int):

moon := temperature["Moon"]
fmt.Println(moon)              1

  • 1 Prints 0

Go provides the comma, ok syntax, which you can use to distinguish between the "Moon" not existing in the map versus being present in the map with a temperature of 0° C:

if moon, ok := temperature["Moon"]; ok {                    1
     fmt.Printf("On average the moon is %v° C.
", moon)
} else {
    fmt.Println("Where is the moon?")                       2
}

  • 1 The comma, ok syntax
  • 2 Prints Where is the moon?

The moon variable will contain the value found at the "Moon" key or the zero value. The additional ok variable will be true if the key is present, or false otherwise.

Note

When using the comma, ok syntax you can use any variable names you like:

temp, found := temperature["Venus"]

Quick check 19.1

1

What type would you use to declare a map with 64-bit floating-point keys and integer values?

2

If you modify listing 19.1 so that the "Moon" key is present with a value of 0, what’s the result of using the comma, ok syntax?

QC 19.1 answer

1

The map type is map[float64]int.

2

The value of ok will be true:

temperature := map[string]int{
    "Earth": 15,
    "Mars": -65,
    "Moon": 0,
}

if moon, ok := temperature["Moon"]; ok {
    fmt.Printf("On average the moon is %v° C.
", moon)      1
} else {
    fmt.Println("Where is the moon?")
}

  • 1 Prints On average the moon is 0° C.

 

19.2. Maps aren’t copied

As you learned in lesson 16, arrays are copied when assigned to new variables or when passed to functions or methods. The same is true for primitive types like int and float64.

Maps behave differently. In the next listing, both planets and planetsMarkII share the same underlying data. As you can see, changes to one impact the other. That’s a bit unfortunate given the circumstances.

Listing 19.2. Pointing at the same data: whoops.go
planets := map[string]string{
    "Earth": "Sector ZZ9",
    "Mars":  "Sector ZZ9",
}

planetsMarkII := planets
planets["Earth"] = "whoops"

fmt.Println(planets)                 1
fmt.Println(planetsMarkII)           1

delete(planets, "Earth")             2
fmt.Println(planetsMarkII)           3

  • 1 Prints map[Earth:whoops Mars:Sector ZZ9]
  • 2 Removes Earth from the map
  • 3 Prints map[Mars:Sector ZZ9]

When the delete built-in function removes an element from the map, both planets and planetsMarkII are impacted by the change. If you pass a map to a function or method, it may alter the contents of the map. This behavior is similar to multiple slices that point to the same underlying array.

Quick check 19.2

1

Why are changes to planets also reflected in planetsMarkII in listing 19.2?

2

What does the delete built-in function do?

QC 19.2 answer

1

The planetsMarkII variable points at the same underlying data as planets.

2

The delete function removes an element from a map.

 

19.3. Preallocating maps with make

Maps are similar to slices in another way. Unless you initialize them with a composite literal, maps need to be allocated with the make built-in function.

For maps, make only accepts one or two parameters. The second one preallocates space for a number of keys, much like capacity for slices. A map’s initial length will always be zero when using make:

temperature := make(map[float64]int, 8)
Quick check 19.3

Q1:

What do you suppose is the benefit of preallocating a map with make?

QC 19.3 answer

1:

As with slices, specifying an initial size for a map can save the computer some work later when the map gets bigger.

 

19.4. Using maps to count things

The code in listing 19.3 determines the frequency of temperatures taken from the MAAS API (github.com/ingenology/mars_weather_api). If frequency were a slice, the keys would need to be integers, and the underlying array would need to reserve space to count temperatures that never actually occur. A map is clearly a better choice in this case.

Listing 19.3. Frequency of temperatures: frequency.go
temperatures := []float64{
    -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

frequency := make(map[float64]int)

for _, t := range temperatures {                       1
     frequency[t]++
}

for t, num := range frequency {                        2
     fmt.Printf("%+.2f occurs %d times
", t, num)
}

  • 1 Iterates over a slice (index, value)
  • 2 Iterates over a map (key, value)

Iteration with the range keyword works similarly for slices, arrays, and maps. Rather than an index and value, maps provide the key and value for each iteration. Be aware that Go doesn’t guarantee the order of map keys, so the output may change from one run to another.

Quick check 19.4

Q1:

When iterating over a map, what are the two variables populated with?

QC 19.4 answer

1:

The key and the value for each element in the map.

 

19.5. Grouping data with maps and slices

Instead of determining the frequency of temperatures, let’s group temperatures together in divisions of 10° each. To do that, the following listing maps from a group to a slice of temperatures in that group.

Listing 19.4. A map of slices: group.go
temperatures := []float64{
    -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

groups := make(map[float64][]float64)        1

for _, t := range temperatures {
    g := math.Trunc(t/10) * 10               2
    groups[g] = append(groups[g], t)
}

for g, temperatures := range groups {
    fmt.Printf("%v: %v
", g, temperatures)
}

  • 1 A map with float64 keys and []float64 values
  • 2 Rounds temperatures down to -20, -30, and so on

The previous listing produces output like this:

30: [32]
-30: [-31 -33]
-20: [-28 -29 -23 -29 -28]

Quick check 19.5

Q1:

What is the type for the keys and values in the declaration var groups map[string][]int?

QC 19.5 answer

1:

The groups map has keys of type string and values that are a slice of integers.

 

19.6. Repurposing maps as sets

A set is a collection similar to an array, except that each element is guaranteed to occur only once. Go doesn’t provide a set collection, but you can always improvise by using a map, as shown in the following listing. The value isn’t important, but true is convenient for checking set membership. If a temperature is present in the map and it has a value of true, it’s a member of the set.

Listing 19.5. A makeshift set: set.go
var temperatures = []float64{
    -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

set := make(map[float64]bool)        1
for _, t := range temperatures {
    set[t] = true
}

if set[-28.0] {
    fmt.Println("set member")        2
}

fmt.Println(set)                     3

  • 1 Makes a map with Boolean values
  • 2 Prints set member
  • 3 Prints map[-31:true -29:true -23:true -33:true -28:true 32:true]

You can see that the map only contains one key for each temperature, with any duplicates removed. But map keys have an arbitrary order in Go, so before they can be sorted, the temperatures must be converted back to a slice:

unique := make([]float64, 0, len(set))
for t := range set {
    unique = append(unique, t)
}
sort.Float64s(unique)

fmt.Println(unique)              1

  • 1 Prints [-33 -31 -29 -28 -23 32]
Quick check 19.6

Q1:

How would you check whether 32.0 is a member of set?

QC 19.6 answer

1:

if set[32.0] {
    // set member
}

 

Summary

  • Maps are versatile collections for unstructured data.
  • Composite literals provide a convenient means to initialize maps.
  • The range keyword can iterate over maps.
  • Maps share the same underlying data when assigned or passed to functions.
  • Collections become more powerful when combined with each other.

Let’s see if you got this...

Experiment: words.go

Write a function to count the frequency of words in a string of text and return a map of words with their counts. The function should convert the text to lowercase, and punctuation should be trimmed from words. The strings package contains several helpful functions for this task, including Fields, ToLower, and Trim.

Use your function to count the frequency of words in the following passage and then display the count for any word that occurs more than once.

As far as eye could reach he saw nothing but the stems of the great plants about him receding in the violet shade, and far overhead the multiple transparency of huge leaves filtering the sunshine to the solemn splendour of twilight in which he walked. Whenever he felt able he ran again; the ground continued soft and springy, covered with the same resilient weed which was the first thing his hands had touched in Malacandra. Once or twice a small red creature scuttled across his path, but otherwise there seemed to be no life stirring in the wood; nothing to fear—except the fact of wandering unprovisioned and alone in a forest of unknown vegetation thousands or millions of miles beyond the reach or knowledge of man.

C.S. Lewis, Out of the Silent Planet, (see mng.bz/V7nO)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset