Lesson 2
Working with Maps in Go: A Beginner's Guide
Introduction

Welcome to our exploration of data structures! Today, we'll dive into Go Maps, a powerful feature that allows you to efficiently manage key-value pairs. Similar to a real-world address book, maps in Go let you quickly locate a "value" by referencing its "key." This lesson will guide you through the fundamental concepts of Go maps, demonstrating how they simplify data access and manipulation.

Go Maps

Our exploration begins by understanding Go maps, a crucial structure for storing data as key-value pairs. Imagine you have a digital contact list where you can look up a friend's phone number (value) using their name (key). In Go, maps offer this functionality succinctly.

To create a map in Go, you use the built-in map type, often in conjunction with the make function. The make function initializes and allocates memory for the requested type, which is map in our case. Here's how you define a map within a PhoneBook type where both keys and values are strings:

Go
1package main 2 3import "fmt" 4 5type PhoneBook struct { 6 contacts map[string]string 7} 8 9func NewPhoneBook() *PhoneBook { 10 return &PhoneBook{ 11 contacts: make(map[string]string), 12 } 13} 14 15func main() { 16 phoneBook := NewPhoneBook() 17}

In the above code, the PhoneBook struct contains a contacts map, which is initialized using make via the NewPhoneBook function. This setup allows for dynamic data storage, where you can later add, update, and retrieve phone numbers efficiently using unique names as keys. The make function ensures that the map is ready for use, with allocated memory to handle these operations.

Operations in Maps

Go provides a range of operations for working with maps: adding, updating, retrieving, and deleting key-value pairs. Understanding these operations is key to effectively using maps in Go.

  • To add or update a map entry, simply assign a value to a key. This action either updates the existing value or creates a new key-value pair if the key doesn't exist.
  • To retrieve a value, access the key directly. The comma-ok idiom is typically used in this context to determine if the key exists in the map. When you access a value from a map, the result is accompanied by a boolean value. The pattern value, ok := myMap[key] helps you safely check for the map's key existence without raising an error if the key isn't found.
  • To delete an entry, use the delete function with the map and key as arguments. While the delete function alone does not raise an error for non-existent keys, you may want to check for the key's existence beforehand to provide custom feedback to the user, such as when confirming whether a task exists or not.

Here's how these map operations work in a Task Manager example:

Go
1package main 2 3import "fmt" 4 5type TaskManager struct { 6 tasks map[string]string 7} 8 9func NewTaskManager() *TaskManager { 10 return &TaskManager{ 11 tasks: make(map[string]string), 12 } 13} 14 15func (tm *TaskManager) addOrUpdateTask(taskName, status string) { 16 // Adds or updates a task 17 tm.tasks[taskName] = status 18} 19 20func (tm *TaskManager) getTaskStatus(taskName string) string { 21 // Retrieves the value associated with a particular task 22 if status, exists := tm.tasks[taskName]; exists { // Using comma-ok idiom 23 return status 24 } 25 return "Not Found" 26} 27 28func (tm *TaskManager) deleteTask(taskName string) { 29 // Deletes task from the map 30 if _, exists := tm.tasks[taskName]; !exists { // Using comma-ok idiom 31 fmt.Printf("Task '%s' not found.\n", taskName) 32 } 33 delete(tm.tasks, taskName) 34} 35 36func main() { 37 myTasks := NewTaskManager() 38 myTasks.addOrUpdateTask("Buy Milk", "Pending") 39 fmt.Println(myTasks.getTaskStatus("Buy Milk")) // Output: Pending 40 myTasks.addOrUpdateTask("Buy Milk", "Completed") 41 fmt.Println(myTasks.getTaskStatus("Buy Milk")) // Output: Completed 42 43 myTasks.deleteTask("Buy Milk") 44 fmt.Println(myTasks.getTaskStatus("Buy Milk")) // Output: Not Found 45}

This example highlights how Go maps facilitate dynamic data updates, retrievals and deletions, and demonstrates the use of the comma-ok idiom to handle key existence checks safely.

Looping Through Maps

Go provides a simple and effective way to iterate over maps using the for range construct. This loop allows you to traverse keys, values, or both. The for range syntax is versatile and can be used as follows: for key, value := range myMap { ... }, where:

  • key: Represents the key of each element in the map during the iteration.
  • value: Denotes the value associated with each key in the map.

Let's see this in action within our Task Manager example:

Go
1package main 2 3import "fmt" 4 5type TaskManager struct { 6 tasks map[string]string 7} 8 9func NewTaskManager() *TaskManager { 10 return &TaskManager{ 11 tasks: make(map[string]string), 12 } 13} 14 15func (tm *TaskManager) addTask(taskName, status string) { 16 tm.tasks[taskName] = status 17} 18 19func (tm *TaskManager) printAllTasks() { 20 for taskName, status := range tm.tasks { 21 fmt.Printf("%s: %s\n", taskName, status) 22 } 23} 24 25func main() { 26 myTasks := NewTaskManager() 27 myTasks.addTask("Buy Milk", "Pending") 28 myTasks.addTask("Pay Bills", "Completed") 29 30 myTasks.printAllTasks() 31}

With for range, we print each task's name (key) with its status (value) in the map. This approach is efficient and straightforward.

Nesting with Maps

Nesting in maps involves embedding one map within another. This technique is useful for associating multiple pieces of data with a single key. Let's explore this concept in a Student Database example. Please note that Go maps do not guarantee any ordering for keys, meaning that data remains unsorted unless sorted explicitly.

Go
1package main 2 3import "fmt" 4 5type StudentDatabase struct { 6 students map[string]map[string]string 7} 8 9func NewStudentDatabase() *StudentDatabase { 10 return &StudentDatabase{ 11 students: make(map[string]map[string]string), 12 } 13} 14 15func (db *StudentDatabase) addStudent(name string, subjects map[string]string) { 16 db.students[name] = subjects 17} 18 19func (db *StudentDatabase) getMark(name, subject string) string { 20 if studentSubjects, exists := db.students[name]; exists { 21 if grade, subjectExists := studentSubjects[subject]; subjectExists { 22 return grade 23 } 24 } 25 return "N/A" 26} 27 28func (db *StudentDatabase) printDatabase() { 29 for student, subjects := range db.students { 30 fmt.Println("Student:", student) 31 for subject, grade := range subjects { 32 fmt.Printf(" Subject: %s, Grade: %s\n", subject, grade) 33 } 34 } 35} 36 37func main() { 38 studentDB := NewStudentDatabase() 39 studentDB.addStudent("Alice", map[string]string{"Math": "A", "English": "B"}) 40 41 fmt.Println(studentDB.getMark("Alice", "English")) // Output: B 42 fmt.Println(studentDB.getMark("Alice", "History")) // Output: N/A 43 44 studentDB.printDatabase() 45}

In this example:

  • The StudentDatabase struct contains a students map, where each key is a student's name and each value is another map containing subjects and grades.
  • NewStudentDatabase initializes and returns a StudentDatabase with an allocated nested map structure.
  • The addStudent method adds or updates a student's record with subjects and their respective grades.
  • The printDatabase method traverses the nested maps to display each student's subjects and grades.
Hands-on Example

Now, let's explore a practical scenario: managing a shopping cart in an online store. This example will illustrate using maps to associate product names with their quantities.

Go
1package main 2 3import "fmt" 4 5type ShoppingCart struct { 6 cart map[string]int 7} 8 9func NewShoppingCart() *ShoppingCart { 10 return &ShoppingCart{ 11 cart: make(map[string]int), 12 } 13} 14 15func (sc *ShoppingCart) addProduct(productName string, quantity int) { 16 sc.cart[productName] += quantity 17} 18 19func (sc *ShoppingCart) removeProduct(productName string) { 20 if _, exists := sc.cart[productName]; exists { 21 delete(sc.cart, productName) 22 } else { 23 fmt.Printf("%s not found in your cart.\n", productName) 24 } 25} 26 27func (sc *ShoppingCart) showCart() { 28 if len(sc.cart) == 0 { 29 fmt.Println("Your shopping cart is empty.") 30 } else { 31 for product, quantity := range sc.cart { 32 fmt.Printf("%s: %d\n", product, quantity) 33 } 34 } 35} 36 37func main() { 38 myCart := NewShoppingCart() 39 myCart.addProduct("Apples", 5) 40 myCart.addProduct("Bananas", 2) 41 myCart.addProduct("Apples", 3) // Updates quantity of apples to 8 42 43 myCart.showCart() 44 45 myCart.removeProduct("Bananas") 46 myCart.showCart() 47}
  • A ShoppingCart struct is defined with a cart field, which is a map where keys are product names and values are quantities.
  • The addProduct method increases the quantity of a product in the cart map, adding to the existing quantity if the product already exists.
  • The removeProduct method checks if a product exists in the cart map; if it does, the product is removed using the delete function.
  • The showCart method iterates over the cart map and prints out all items and their quantities, or informs if the cart is empty.
  • The main function demonstrates functionality by adding products to the cart, updating quantities, printing the cart contents, and removing products.

This example demonstrates the practicality of Go maps for managing dynamic datasets, such as a shopping cart, enabling efficient data operations.

Lesson Summary and Practice

Congratulations! You've now explored Go maps and learned how to manipulate them effectively. We encourage you to dive into practice exercises to enhance your understanding of Go maps. Practice is crucial for mastering these concepts. Enjoy your learning journey!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.