Lesson 2
Managing User Data with Filtering and Aggregation in Go
Introduction

Welcome to today's lesson on applying data filtering and aggregation in a real-world scenario using a user management system in Go. We'll start by building a foundational structure that can handle basic user operations. Then, we'll expand it by introducing more advanced functionalities that allow filtering and aggregating user data.

Starter Task Methods

In our starter task, we will implement a set of functions that manage basic operations on a collection of user data, specifically handling adding new users, retrieving user profiles, and updating user profiles.

Here are the starter task methods:

  • addUser(userID string, age int, country string, subscribed bool) bool - Adds a new user with the specified attributes. The parameters are passed by value since you're providing complete new values for the addition, and there's no need to track changes after the function call. Returns true if the user was added successfully and false if a user with the same userID already exists.
  • getUser(userID string) *UserProfile - Returns a pointer to the user's profile if the user exists; otherwise, returns nil.
  • updateUser(userID string, age *int, country *string, subscribed *bool) bool - Updates the user's profile based on non-nil parameters. Differently from addUser, notice the use of pointers which allows for selective updates; by passing a nil, you indicate that a specific field should remain unchanged. Returns true if the user exists and was updated; false otherwise.

To store the user data, we will define a UserProfile struct.

Starter Task Implementation

Here is the implementation of our starter task in Go:

Go
1package main 2 3import ( 4 "fmt" 5) 6 7// UserProfile struct to store user details 8type UserProfile struct { 9 Age int 10 Country string 11 Subscribed bool 12} 13 14// UserManager to manage user profiles 15type UserManager struct { 16 users map[string]UserProfile 17} 18 19// NewUserManager creates a new UserManager 20func NewUserManager() *UserManager { 21 return &UserManager{users: make(map[string]UserProfile)} 22} 23 24// AddUser adds a new user 25func (um *UserManager) addUser(userID string, age int, country string, subscribed bool) bool { 26 if _, exists := um.users[userID]; exists { 27 return false 28 } 29 um.users[userID] = UserProfile{Age: age, Country: country, Subscribed: subscribed} 30 return true 31} 32 33// GetUser retrieves a user profile 34func (um *UserManager) getUser(userID string) *UserProfile { 35 if user, exists := um.users[userID]; exists { 36 return &user 37 } 38 return nil 39} 40 41// UpdateUser updates a user's profile 42func (um *UserManager) updateUser(userID string, age *int, country *string, subscribed *bool) bool { 43 if user, exists := um.users[userID]; exists { 44 if age != nil { 45 user.Age = *age 46 } 47 if country != nil { 48 user.Country = *country 49 } 50 if subscribed != nil { 51 user.Subscribed = *subscribed 52 } 53 um.users[userID] = user 54 return true 55 } 56 return false 57} 58 59func main() { 60 um := NewUserManager() 61 fmt.Println(um.addUser("u1", 25, "USA", true)) // true 62 fmt.Println(um.addUser("u2", 30, "Canada", false)) // true 63 fmt.Println(um.addUser("u1", 22, "Mexico", true)) // false 64 65 user := um.getUser("u1") 66 if user != nil { 67 fmt.Println(user.Age) // 25 68 } 69 70 fmt.Println(um.updateUser("u1", intPointer(26), nil, nil)) // true 71 fmt.Println(um.updateUser("u3", intPointer(19), stringPointer("UK"), boolPointer(false))) // false 72} 73 74func intPointer(i int) *int { return &i } 75func stringPointer(s string) *string { return &s } 76func boolPointer(b bool) *bool { return &b }

The code provides a basic user management system in Go:

  • It uses the UserProfile struct to store user details such as age, country, and subscription status.
  • The UserManager struct manages user profiles in a map, with user IDs as keys.
  • The NewUserManager function initializes UserManager with an empty map of users.
  • Helper functions (intPointer, stringPointer, boolPointer) are included to allow optional parameters to be easily passed when updating user profiles.
  • In the main function, users are added, retrieved, and updated to demonstrate the functionality of the user management system.
Step 1: Adding 'filterUsers' Method

This method filters users based on the criteria provided. Let's see how it works:

Go
1/// ... previous code 2 3// FilterUsers filters users based on criteria 4func (um *UserManager) filterUsers(minAge, maxAge *int, country *string, subscribed *bool) []string { 5 filteredUsers := []string{} 6 7 for userID, profile := range um.users { 8 if minAge != nil && profile.Age < *minAge { 9 continue 10 } 11 if maxAge != nil && profile.Age > *maxAge { 12 continue 13 } 14 if country != nil && profile.Country != *country { 15 continue 16 } 17 if subscribed != nil && profile.Subscribed != *subscribed { 18 continue 19 } 20 filteredUsers = append(filteredUsers, userID) 21 } 22 return filteredUsers 23} 24 25// Example usage of the new method 26func main() { 27 um := NewUserManager() 28 um.addUser("u1", 25, "USA", true) 29 um.addUser("u2", 30, "Canada", false) 30 um.addUser("u3", 22, "USA", true) 31 32 result1 := um.filterUsers(intPointer(20), intPointer(30), stringPointer("USA"), boolPointer(true)) 33 fmt.Println(result1) // [u1 u3] 34 35 result2 := um.filterUsers(nil, intPointer(28), nil, nil) 36 fmt.Println(result2) // [u1 u3] 37 38 result3 := um.filterUsers(nil, nil, stringPointer("Canada"), boolPointer(false)) 39 fmt.Println(result3) // [u2] 40}

In this code snippet:

  • The filterUsers method filters users based on minAge, maxAge, country, and subscribed status criteria.
  • It iterates over the users map and checks each user's profile against the provided criteria.
  • Users who meet all the criteria are added to the filteredUsers list, which is then returned.
  • The example usage shows how to add users and filter them based on different criteria.
Step 2: Adding 'aggregateStats' Method

This method aggregates statistics from the user profiles. Let's implement it:

Go
1// ... previous code 2 3// aggregateStats aggregates statistics about users 4func (um *UserManager) aggregateStats() map[string]float64 { 5 totalUsers := len(um.users) 6 if totalUsers == 0 { 7 return map[string]float64{"total_users": 0, "average_age": 0, "subscribed_ratio": 0.0} 8 } 9 10 var totalAge int 11 var subscribedCount int 12 for _, profile := range um.users { 13 totalAge += profile.Age 14 if profile.Subscribed { 15 subscribedCount++ 16 } 17 } 18 19 averageAge := float64(totalAge / totalUsers) 20 subscribedRatio := float64(subscribedCount) / float64(totalUsers) 21 22 return map[string]float64{ 23 "total_users": float64(totalUsers), 24 "average_age": averageAge, 25 "subscribed_ratio": subscribedRatio, 26 } 27} 28 29func main() { 30 um := NewUserManager() 31 um.addUser("u1", 25, "USA", true) 32 um.addUser("u2", 30, "Canada", false) 33 um.addUser("u3", 22, "USA", true) 34 35 stats := um.aggregateStats() 36 fmt.Printf("Total users: %.0f\n", stats["total_users"]) // 3 37 fmt.Printf("Average age: %.0f\n", stats["average_age"]) // 25 38 fmt.Printf("Subscribed ratio: %.2f\n", stats["subscribed_ratio"]) // 0.67 39}

Let's discuss this new functionality:

  • The aggregateStats method calculates and returns aggregate statistics about the users in the form of a map.
  • It determines totalUsers as the total number of users.
  • If there are no users, it returns a map with zeroed statistics.
  • It calculates totalAge by summing the ages of all users.
  • It computes subscribedCount by counting the users who are subscribed.
  • It computes averageAge by dividing totalAge by totalUsers.
  • It calculates subscribedRatio by dividing subscribedCount by totalUsers.
  • The resulting statistics map includes total_users, average_age, and subscribed_ratio.
Lesson Summary

Great job! Today, you've learned how to effectively handle user data in Go by implementing advanced functionalities like filtering and aggregation on top of a basic system. This is a critical skill in real-life software development, where you often need to extend existing systems to meet new requirements.

I encourage you to practice solving similar challenges to solidify your understanding of data filtering and aggregation in Go. Happy coding, and see you in the next lesson!

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