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:
- Merge: Combine two maps
- Keys: Extract all keys from a map into a slice
- Values: Extract all values from a map into a slice
- Invert: Swap keys and values
- 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
- All functions should handle empty maps gracefully
- Merge should not modify the original maps
- Keys and Values should return empty slices for empty maps
- Invert should handle the case where values are not unique
- 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
- 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
-
Additional Operations:
Has: Check if a key exists in the mapPick: Extract a subset of keys from a mapOmit: Create a map excluding specified keysMapValues: Transform all values using a function
-
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}
- 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.