Map Explorer

Exercise: Map Explorer

Difficulty - Beginner

Learning Objectives

  • Master Go map operations
  • Understand map initialization and manipulation
  • Practice key existence checking
  • Learn map iteration patterns

Problem Statement

Create a MapUtils package that implements various map operations commonly used in Go programs:

  1. Merge: Combine two maps
  2. Keys: Extract all keys from a map into a slice
  3. Values: Extract all values from a map into a slice
  4. Invert: Swap keys and values
  5. Filter: Keep only key-value pairs that satisfy a condition

Function Signatures

 1package maputils
 2
 3// Merge combines two maps, with values from map2 overwriting map1 on collision
 4func Merge(map1, map2 map[string]int) map[string]int
 5
 6// Keys returns all keys from the map
 7func Keys(m map[string]int) []string
 8
 9// Values returns all values from the map
10func Values(m map[string]int) []int
11
12// Invert swaps keys and values
13func Invert(m map[string]int) map[int]string
14
15// Filter returns a new map with only entries that satisfy the predicate
16func Filter(m map[string]int, predicate func(string, int) bool) map[string]int

Example Usage

 1package main
 2
 3import (
 4    "fmt"
 5    "maputils"
 6)
 7
 8func main() {
 9    // Merge
10    map1 := map[string]int{"a": 1, "b": 2}
11    map2 := map[string]int{"b": 3, "c": 4}
12    merged := maputils.Merge(map1, map2)
13    fmt.Println(merged) // map[a:1 b:3 c:4]
14
15    // Keys
16    m := map[string]int{"apple": 5, "banana": 3, "cherry": 7}
17    keys := maputils.Keys(m)
18    fmt.Println(keys) // [apple banana cherry]
19
20    // Values
21    values := maputils.Values(m)
22    fmt.Println(values) // [5 3 7]
23
24    // Invert
25    original := map[string]int{"one": 1, "two": 2, "three": 3}
26    inverted := maputils.Invert(original)
27    fmt.Println(inverted) // map[1:one 2:two 3:three]
28
29    // Filter
30    scores := map[string]int{
31        "Alice":   85,
32        "Bob":     72,
33        "Charlie": 90,
34        "David":   65,
35    }
36    passing := maputils.Filter(scores, func(name string, score int) bool {
37        return score >= 70
38    })
39    fmt.Println(passing) // map[Alice:85 Bob:72 Charlie:90]
40}

Requirements

  1. All functions should handle empty maps gracefully
  2. Merge should not modify the original maps
  3. Keys and Values should return empty slices for empty maps
  4. Invert should handle the case where values are not unique
  5. Filter should return a new map without modifying the original

Test Cases

  1package maputils
  2
  3import (
  4    "reflect"
  5    "sort"
  6    "testing"
  7)
  8
  9func TestMerge(t *testing.T) {
 10    tests := []struct {
 11        map1     map[string]int
 12        map2     map[string]int
 13        expected map[string]int
 14    }{
 15        {
 16            map[string]int{"a": 1, "b": 2},
 17            map[string]int{"b": 3, "c": 4},
 18            map[string]int{"a": 1, "b": 3, "c": 4},
 19        },
 20        {
 21            map[string]int{},
 22            map[string]int{"a": 1},
 23            map[string]int{"a": 1},
 24        },
 25        {
 26            map[string]int{"a": 1},
 27            map[string]int{},
 28            map[string]int{"a": 1},
 29        },
 30    }
 31
 32    for _, tt := range tests {
 33        result := Merge(tt.map1, tt.map2)
 34        if !reflect.DeepEqual(result, tt.expected) {
 35            t.Errorf("Merge(%v, %v) = %v; want %v", tt.map1, tt.map2, result, tt.expected)
 36        }
 37    }
 38}
 39
 40func TestKeys(t *testing.T) {
 41    tests := []struct {
 42        input    map[string]int
 43        expected []string
 44    }{
 45        {map[string]int{"a": 1, "b": 2, "c": 3}, []string{"a", "b", "c"}},
 46        {map[string]int{"x": 10}, []string{"x"}},
 47        {map[string]int{}, []string{}},
 48    }
 49
 50    for _, tt := range tests {
 51        result := Keys(tt.input)
 52        sort.Strings(result)
 53        sort.Strings(tt.expected)
 54
 55        if !reflect.DeepEqual(result, tt.expected) {
 56            t.Errorf("Keys(%v) = %v; want %v", tt.input, result, tt.expected)
 57        }
 58    }
 59}
 60
 61func TestValues(t *testing.T) {
 62    tests := []struct {
 63        input    map[string]int
 64        expected []int
 65    }{
 66        {map[string]int{"a": 1, "b": 2, "c": 3}, []int{1, 2, 3}},
 67        {map[string]int{"x": 10}, []int{10}},
 68        {map[string]int{}, []int{}},
 69    }
 70
 71    for _, tt := range tests {
 72        result := Values(tt.input)
 73        sort.Ints(result)
 74        sort.Ints(tt.expected)
 75
 76        if !reflect.DeepEqual(result, tt.expected) {
 77            t.Errorf("Values(%v) = %v; want %v", tt.input, result, tt.expected)
 78        }
 79    }
 80}
 81
 82func TestInvert(t *testing.T) {
 83    tests := []struct {
 84        input    map[string]int
 85        expected map[int]string
 86    }{
 87        {
 88            map[string]int{"one": 1, "two": 2, "three": 3},
 89            map[int]string{1: "one", 2: "two", 3: "three"},
 90        },
 91        {
 92            map[string]int{"a": 1},
 93            map[int]string{1: "a"},
 94        },
 95        {
 96            map[string]int{},
 97            map[int]string{},
 98        },
 99    }
100
101    for _, tt := range tests {
102        result := Invert(tt.input)
103        if !reflect.DeepEqual(result, tt.expected) {
104            t.Errorf("Invert(%v) = %v; want %v", tt.input, result, tt.expected)
105        }
106    }
107}
108
109func TestFilter(t *testing.T) {
110    isPositive := func(k string, v int) bool { return v > 0 }
111    startsWithA := func(k string, v int) bool { return len(k) > 0 && k[0] == 'a' }
112
113    tests := []struct {
114        input     map[string]int
115        predicate func(string, int) bool
116        expected  map[string]int
117    }{
118        {
119            map[string]int{"a": 1, "b": -2, "c": 3},
120            isPositive,
121            map[string]int{"a": 1, "c": 3},
122        },
123        {
124            map[string]int{"apple": 5, "banana": 3, "apricot": 7},
125            startsWithA,
126            map[string]int{"apple": 5, "apricot": 7},
127        },
128        {
129            map[string]int{},
130            isPositive,
131            map[string]int{},
132        },
133    }
134
135    for _, tt := range tests {
136        result := Filter(tt.input, tt.predicate)
137        if !reflect.DeepEqual(result, tt.expected) {
138            t.Errorf("Filter(%v) = %v; want %v", tt.input, result, tt.expected)
139        }
140    }
141}

Hints

Hint 1: Merge

Create a new map and copy all entries from the first map. Then iterate over the second map and add/overwrite entries in the result map.

1result := make(map[string]int, len(map1)+len(map2))
Hint 2: Keys

Create a slice with capacity equal to the map length. Iterate over the map and append each key to the slice.

1keys := make([]string, 0, len(m))
2for key := range m {
3    keys = append(keys, key)
4}
Hint 3: Values

Similar to Keys, but append the values instead of keys.

Hint 4: Invert

Create a new map with reversed key-value types. Iterate over the original map and insert values as keys and keys as values.

Hint 5: Filter

Create a new map and only add entries where the predicate function returns true.


Solution

Click to see the complete solution
 1package maputils
 2
 3// Merge combines two maps, with values from map2 overwriting map1 on collision
 4func Merge(map1, map2 map[string]int) map[string]int {
 5    // Pre-allocate with estimated size
 6    result := make(map[string]int, len(map1)+len(map2))
 7
 8    // Copy all entries from map1
 9    for key, value := range map1 {
10        result[key] = value
11    }
12
13    // Add/overwrite with entries from map2
14    for key, value := range map2 {
15        result[key] = value
16    }
17
18    return result
19}
20
21// Keys returns all keys from the map
22func Keys(m map[string]int) []string {
23    if len(m) == 0 {
24        return []string{}
25    }
26
27    keys := make([]string, 0, len(m))
28    for key := range m {
29        keys = append(keys, key)
30    }
31
32    return keys
33}
34
35// Values returns all values from the map
36func Values(m map[string]int) []int {
37    if len(m) == 0 {
38        return []int{}
39    }
40
41    values := make([]int, 0, len(m))
42    for _, value := range m {
43        values = append(values, value)
44    }
45
46    return values
47}
48
49// Invert swaps keys and values
50func Invert(m map[string]int) map[int]string {
51    result := make(map[int]string, len(m))
52
53    for key, value := range m {
54        result[value] = key
55    }
56
57    return result
58}
59
60// Filter returns a new map with only entries that satisfy the predicate
61func Filter(m map[string]int, predicate func(string, int) bool) map[string]int {
62    result := make(map[string]int)
63
64    for key, value := range m {
65        if predicate(key, value) {
66            result[key] = value
67        }
68    }
69
70    return result
71}

Explanation

Merge:

  • Pre-allocates result map with estimated size for efficiency
  • Copies all entries from first map, then second map
  • Second map values automatically overwrite first map values on key collision
  • Time complexity: O(n + m), Space complexity: O(n + m)

Keys:

  • Returns empty slice for empty maps
  • Pre-allocates slice with exact capacity to avoid reallocation
  • Order of keys is non-deterministic
  • Time complexity: O(n), Space complexity: O(n)

Values:

  • Similar to Keys but extracts values instead
  • Also returns empty slice for empty maps
  • Values may contain duplicates if original map has duplicate values

Invert:

  • Creates reversed map where original values become keys
  • If duplicate values exist, last occurrence wins
  • Useful for creating bidirectional lookups
  • Time complexity: O(n), Space complexity: O(n)

Filter:

  • Uses predicate function for flexible filtering logic
  • Creates new map without modifying original
  • Pre-allocation not possible as we don't know how many will match
  • Time complexity: O(n), Space complexity: O(k) where k is matches

Common Pitfalls

1. Modifying the original map:

1// Wrong: Modifies original
2func MergeBad(map1, map2 map[string]int) map[string]int {
3    for k, v := range map2 {
4        map1[k] = v // Modifies map1!
5    }
6    return map1
7}

2. Returning nil instead of empty slice:

1// Less ideal: Returns nil for empty map
2func KeysBad(m map[string]int) []string {
3    var keys []string // nil slice
4    for key := range m {
5        keys = append(keys, key)
6    }
7    return keys // returns nil if map is empty
8}

3. Not checking for nil maps:

1// Safer version with nil check
2func KeysSafe(m map[string]int) []string {
3    if m == nil || len(m) == 0 {
4        return []string{}
5    }
6    // ... rest of implementation
7}

Alternative Implementations

Keys:

1import "sort"
2
3func KeysSorted(m map[string]int) []string {
4    keys := Keys(m)
5    sort.Strings(keys)
6    return keys
7}

Merge:

1func MergeMultiple(maps ...map[string]int) map[string]int {
2    result := make(map[string]int)
3    for _, m := range maps {
4        for k, v := range m {
5            result[k] = v
6        }
7    }
8    return result
9}

Bonus Challenges

  1. Generic Version: Implement using Go generics to work with any map types
1func Merge[K comparable, V any](map1, map2 map[K]V) map[K]V
2func Keys[K comparable, V any](m map[K]V) []K
3func Values[K comparable, V any](m map[K]V) []V
  1. Additional Operations:

    • Has: Check if a key exists in the map
    • Pick: Extract a subset of keys from a map
    • Omit: Create a map excluding specified keys
    • MapValues: Transform all values using a function
  2. Thread-Safe Map: Create a concurrent-safe map using sync.RWMutex

 1type SafeMap struct {
 2    mu sync.RWMutex
 3    m  map[string]int
 4}
 5
 6func Set(key string, value int) {
 7    sm.mu.Lock()
 8    defer sm.mu.Unlock()
 9    sm.m[key] = value
10}
11
12func Get(key string) {
13    sm.mu.RLock()
14    defer sm.mu.RUnlock()
15    val, ok := sm.m[key]
16    return val, ok
17}
  1. Deep Merge: Implement merge for nested maps
1func DeepMerge(map1, map2 map[string]interface{}) map[string]interface{}

Key Takeaways

  • Maps are reference types - assigning a map to another variable doesn't copy it
  • Map iteration order is random - never rely on iteration order
  • Zero value of a map is nil - nil maps can be read from but not written to
  • Check key existence using the comma-ok idiom: value, exists := m[key]
  • Pre-allocate maps when you know the approximate size: make(map[K]V, size)
  • Delete keys using the built-in delete(m, key) function

Maps are one of Go's most powerful built-in data structures. Understanding map operations thoroughly will help you write more idiomatic and efficient Go code.