golangdata structures

Maps

Imagine a dictionary. You have a word (the key) and its definition (the value). You don't have to scan through every page to find a definition; you jump straight to the word. That's precisely how a map (also known as a hash map or hash table) works in computer science!

Maps are powerful data structures designed for lightning-fast access to stored data. They achieve this by associating keys with values. When you provide a key, a special hash function crunches that key to calculate a unique memory location (or index) where the corresponding value is stored. This clever trick gives maps their superpower: constant-time average performance for insertions, deletions, and read operations. While the hashing process itself isn't "free," the benefits for lookup speed are usually well worth it.


Declaring and Initializing Maps in Go

Go makes working with maps intuitive. Here's how you declare and initialize them:

Basic Declaration

The simplest way to declare an empty map is:

m := map[string]string{} // A map where both keys and values are strings

Here, map[string]string specifies that the keys will be string types, and the values will also be string types.

Initialization with Data

You can also initialize a map with some starting key-value pairs:

m := map[string]string{"hello": "world", "foo": "bar"}

Using make() for Empty Maps (with Capacity Hint)

For an empty map, you can also use the built-in make function, optionally providing a "capacity hint" for performance optimization if you know roughly how many elements you'll store:

m := make(map[string]string)        // An empty map
largeMap := make(map[string]int, 100) // An empty map, pre-allocating space for ~100 entries

Accessing and Modifying Map Data

Once you have a map, interacting with its data is straightforward.

Accessing Values by Key

You retrieve a value using its key, similar to how you'd access an element in an array with an index:

myMap := map[string]string{"name": "Alice", "city": "New York"}
value := myMap["name"] // value will be "Alice"

Checking for Key Existence (The "Comma Ok" Idiom)

What happens if you try to access a key that doesn't exist? The value returned will be the zero value for the map's value type (e.g., 0 for integers, false for booleans, "" for strings, nil for pointers). To distinguish between a missing key and a key whose value is the zero value, Go provides a handy "comma ok" idiom:

value, ok := myMap["age"] // If "age" is not in myMap, value will be "" (zero value for string), and ok will be false.
                          // If "age" was in myMap and its value was "", value would be "" and ok would be true.

if ok {
    fmt.Printf("The value for 'age' is: %s\n", value)
} else {
    fmt.Println("'age' key does not exist in the map.")
}

Adding or Updating Key-Value Pairs

To add a new key-value pair or update an existing one, you use the same assignment syntax:

myMap["greeting"] = "hello" // Adds a new entry: "greeting": "hello"
myMap["name"] = "Bob"       // Updates the existing entry: "name" is now "Bob"

Important Note on Map Keys

The key in a map must be a comparable type. This means the type must support equality (==) and inequality (!=) checks. Go's comparable types include:

  • All basic types: bool, numeric types (int, float64, etc.), string.
  • Pointers.
  • Channel types.
  • Arrays of comparable types.
  • Structs where all their fields are comparable types.

You cannot use slices, maps, or functions as map keys directly because they are not comparable.

Removing a Key-Value Pair

To remove an entry from a map, use the built-in delete function:

delete(myMap, "city") // Removes the key "city" and its corresponding value

Iterating Over Maps

You can easily iterate over the keys, or both keys and values, of a map using a for...range loop:

Iterating Over Keys Only

for k := range myMap {
    fmt.Printf("Key: %s\n", k)
}

Iterating Over Keys and Values

for k, v := range myMap {
    fmt.Printf("Key: %s, Value: %s\n", k, v)
}

It's important to remember that map iteration order is not guaranteed to be the same between runs, or even within the same run if the map is modified. Also, iterating over an empty or nil map is perfectly safe; the loop simply won't execute any iterations.


Real-World Example: Counting Word Occurrences

Maps are perfect for counting things! Let's use a map to count how many times each word appears in a famous passage from Frank Herbert's Dune:

package main

import (
	"fmt"
	"strings" // Import the strings package for Split and Trim
)

func main() {
	p := "I must not fear. Fear is the mind-killer. " +
		"Fear is the little-death that brings total obliteration. " +
		"I will face my fear. I will permit it to pass over me and through me. " +
		"And when it has gone past I will turn the inner eye to see its path. " +
		"Where the fear has gone there will be nothing. Only I will remain."

	// Declare a map to store word counts: key (string) -> word, value (int) -> count
	m := map[string]int{}

	// Split the paragraph into words using space as a delimiter
	words := strings.Split(p, " ")

	// Iterate over each word
	for _, w := range words {
		w = strings.Trim(w, ",.")  // Remove punctuation (commas and periods)
		w = strings.ToLower(w)     // Convert the word to lowercase for consistent counting

		if w == "" { // Handle cases where trimming results in an empty string (e.g., multiple spaces)
            continue
        }

		m[w]++ // Increment the count for this word.
		      // If the word isn't in the map yet, Go automatically
		      // initializes its value to 0 before incrementing.
	}

	fmt.Println(m)
	// Example output (order may vary):
	// map[and:2 be:1 brings:1 eye:1 face:1 fear:5 gone:2 has:2 i:5 inner:1 is:2 it:2
	// its:1 little-death:1 me:2 mind-killer:1 must:1 my:1 not:1 nothing:1 obliteration:1
	// only:1 over:1 pass:1 past:1 path:1 permit:1 remain:1 see:1 that:1 the:4 there:1
	// through:1 to:2 total:1 turn:1 when:1 where:1 will:5]
}

In this example:

  1. We define our paragraph p.
  2. We initialize an empty map m where keys are string (words) and values are int (counts).
  3. strings.Split(p, " ") breaks the paragraph into individual words.
  4. Inside the loop, strings.Trim(w, ",.") removes leading/trailing commas and periods, and strings.ToLower(w) converts words to lowercase, ensuring "Fear" and "fear" are counted as the same word.
  5. m[w]++ is the magic: if w isn't in the map, it's added with a value of 0 before being incremented to 1. If it already exists, its count is simply increased.

From the output, we can easily see which words appeared most frequently in the passage!


Conclusion

Maps are an indispensable tool in Go for managing collections of data that need to be accessed, inserted, or deleted quickly based on a unique key. Their efficiency makes them suitable for a wide range of applications, from caching and frequency counting to building more complex data structures.

Ready to put maps to use in your next Go project? What kind of key-value data will you be organizing?