After reading lesson 19, you’ll be able to
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.
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.
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?
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.
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.
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
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
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 }
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.
When using the comma, ok syntax you can use any variable names you like:
temp, found := temperature["Venus"]
What type would you use to declare a map with 64-bit floating-point keys and integer values?
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?
The map type is map[float64]int.
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.
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.
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
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.
Why are changes to planets also reflected in planetsMarkII in listing 19.2?
What does the delete built-in function do?
The planetsMarkII variable points at the same underlying data as planets.
The delete function removes an element from a map.
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)
What do you suppose is the benefit of preallocating a map with make?
As with slices, specifying an initial size for a map can save the computer some work later when the map gets bigger.
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.
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) }
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.
When iterating over a map, what are the two variables populated with?
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.
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) }
The previous listing produces output like this:
30: [32] -30: [-31 -33] -20: [-28 -29 -23 -29 -28]
What is the type for the keys and values in the declaration var groups map[string][]int?
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.
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
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
How would you check whether 32.0 is a member of set?
Let’s see if you got this...
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)