Lesson 2
Managing Contacts with Maps in Go: Building an Address Book Application
Introduction

Welcome! Today, we'll explore building a simple address book application using Go and its built-in map type. Maps in Go are versatile and allow us to manage collections of key-value pairs efficiently. This task will help you understand how to handle map operations in Go, focusing on adding, retrieving, and deleting entries. By the end of this lesson, you'll have a solid grasp of these fundamental operations and understand the practical significance of Go's map in real-world applications.

Introducing Functions to Implement

In this task, we will implement three functions to manage our address book:

  • func (ab *AddressBook) addContact(name, phoneNumber string) bool: Adds a new contact. Returns false if the contact already exists; otherwise, adds the contact and returns true. In this task, phone numbers are immutable, so overwriting an existing contact's number is not allowed.
  • func (ab *AddressBook) getContact(name string) string: Retrieves the phone number for a given name. Returns an empty string if the contact does not exist.
  • func (ab *AddressBook) deleteContact(name string) bool: Deletes a contact with the given name. Returns true if the contact exists and is deleted, false otherwise.

With these functions, we aim to illustrate a common use case for maps: managing sets of data where each entry is uniquely identified by a key, with the map efficiently handling insertions, deletions, and lookups. Let's break down each function in detail in the next sections.

Step 1: Implementing addContact

This function adds a new contact to the address book with the given name and phoneNumber. If the contact already exists, it returns false. Otherwise, it adds the contact and returns true.

Question: Why do you think we need to check if the contact already exists?

Answer: To avoid overwriting pre-existing entries. If a contact with the same name already exists, we shouldn't allow overwriting its phone number in this method, as contact names are unique identifiers within our map.

Here is the function implementation:

Go
1package main 2 3import ( 4 "fmt" 5) 6 7type AddressBook struct { 8 contacts map[string]string 9} 10 11func (ab *AddressBook) addContact(name, phoneNumber string) bool { 12 if _, exists := ab.contacts[name]; exists { 13 return false 14 } 15 ab.contacts[name] = phoneNumber 16 return true 17} 18 19func (ab *AddressBook) printContacts() { 20 for name, phoneNumber := range ab.contacts { 21 fmt.Printf("%s: %s\n", name, phoneNumber) 22 } 23} 24 25// Example usage: 26func main() { 27 addressBook := &AddressBook{contacts: make(map[string]string)} 28 fmt.Println(addressBook.addContact("Alice", "123-456-7890")) // true 29 fmt.Println(addressBook.addContact("Alice", "098-765-4321")) // false 30 addressBook.printContacts() // Alice: 123-456-7890 31}

In this function:

  • We use the comma, ok idiom (_, exists := ab.contacts[name]) to check if the contact already exists.
  • If it does exist, we return false; otherwise, we add it to our map and return true.
  • This ensures the integrity of each contact, enforcing a no-duplicate rule.
Step 2: Implementing getContact

This function retrieves the phone number associated with a given name. If the contact does not exist, it returns an empty string.

Question: Why is it common to return an empty string in Go when a contact doesn't exist?

Answer: Returning an empty string serves as a clear indicator that the contact is not in the address book. It allows the calling code to handle such cases without exceptions and makes the absence of a contact evident. This choice fits well with Go’s philosophy of simplicity and straightforward handling of errors or non-existent entries.

Here is the function implementation:

Go
1package main 2 3import ( 4 "fmt" 5) 6 7type AddressBook struct { 8 contacts map[string]string 9} 10 11func (ab *AddressBook) addContact(name, phoneNumber string) bool { 12 if _, exists := ab.contacts[name]; exists { 13 return false 14 } 15 ab.contacts[name] = phoneNumber 16 return true 17} 18 19func (ab *AddressBook) getContact(name string) string { 20 if phoneNumber, exists := ab.contacts[name]; exists { 21 return phoneNumber 22 } 23 return "" 24} 25 26// Example usage: 27func main() { 28 addressBook := &AddressBook{contacts: make(map[string]string)} 29 addressBook.addContact("Alice", "123-456-7890") 30 contact := addressBook.getContact("Alice") 31 if contact != "" { 32 fmt.Println(contact) // 123-456-7890 33 } else { 34 fmt.Println("Contact not found") 35 } 36 37 contact = addressBook.getContact("Bob") 38 if contact != "" { 39 fmt.Println(contact) 40 } else { 41 fmt.Println("Contact not found") // Contact not found 42 } 43}

In this function:

  • We use the comma, ok idiom to check for existence. This is efficient, ensuring we only traverse the map once.
  • If the name doesn't exist, we return an empty string; otherwise, the phone number is returned, allowing external code to differentiate easily between valid and non-existent contacts.
Step 3: Implementing deleteContact

This function deletes a contact with the given name. If the contact exists and is deleted, it returns true. If the contact does not exist, it returns false.

Question: What could be a real-world consequence of not checking if the contact exists before deletion?

Answer: Attempting to delete a non-existent contact might lead to confusion or errors if the program assumes the deletion was successful. This is particularly relevant in applications where feedback is necessary to ensure data integrity or notify users of state changes, making the function robust against such erroneous operations.

Here is the function implementation:

Go
1package main 2 3import ( 4 "fmt" 5) 6 7type AddressBook struct { 8 contacts map[string]string 9} 10 11func (ab *AddressBook) addContact(name, phoneNumber string) bool { 12 if _, exists := ab.contacts[name]; exists { 13 return false 14 } 15 ab.contacts[name] = phoneNumber 16 return true 17} 18 19func (ab *AddressBook) getContact(name string) string { 20 if phoneNumber, exists := ab.contacts[name]; exists { 21 return phoneNumber 22 } 23 return "" 24} 25 26func (ab *AddressBook) deleteContact(name string) bool { 27 if _, exists := ab.contacts[name]; exists { 28 delete(ab.contacts, name) 29 return true 30 } 31 return false 32} 33 34// Example usage: 35func main() { 36 addressBook := &AddressBook{contacts: make(map[string]string)} 37 addressBook.addContact("Alice", "123-456-7890") 38 fmt.Println(addressBook.deleteContact("Alice")) // true 39 fmt.Println(addressBook.deleteContact("Bob")) // false 40 addressBook.printContacts() // (empty) 41}

In this function:

  • We verify if the contact exists using the comma, ok idiom.
  • If it exists, we delete it using the delete function and return true; otherwise, we simply return false.
  • This implementation maintains clarity in operations, ensuring users of this function have accurate information about changes performed on the address book.
Why Maps are a Good Choice for These Tasks

Go's maps are particularly efficient for managing an address book due to several reasons:

  • Efficient Lookups: Maps provide average O(1) time complexity for lookups. This means retrieving a contact's phone number by name is very fast, even with a large number of contacts. This is crucial for real-time applications where performance is critical.
  • Simplicity: The map data structure in Go is straightforward and easy to use, providing clean syntax for operations. This simplifies the codebase and reduces the possibility of bugs.
  • Readability: The syntax for accessing map entries is highly readable, making the code easy to understand and maintain. This design choice reflects Go's commitment to clear and maintainable code.
  • Flexibility: Go's maps can hold various data types as values, allowing for easy extension of the address book to store additional information, such as email addresses, if needed in the future. This extensibility makes the map versatile for different scenarios beyond just tiny datasets.

These characteristics make Go's map an ideal choice for implementing an address book and other similar applications where easy access, manipulation, and scalability of data are required.

Lesson Summary

In this lesson, we created a simple address book application using Go's map type. We implemented functions to add, retrieve, and delete contacts. Each step built upon the previous one, enhancing our understanding of working with maps in Go and showcasing their efficacy in managing data. By diving deep into each function, we highlighted the efficiency and versatility of maps, providing a strong foundation for further exploring advanced data structures. Happy coding!

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