Go is known for its simplicity and efficiency. However, one limitation that Go developers have faced for years is the lack of generics. This limitation often led to repetitive code when working with slices of structs, especially when you wanted to group them by a specific property. Thankfully, with the introduction of generics in Go 1.18, this task has become much cleaner and more elegant. In this blog post, we'll explore how to group a slice of structs by a specific property using generics.

The Problem

Imagine you have a slice of structs representing people, and you want to group them by their city of residence. Each person struct might look something like this:

type Person struct {
    Name string
    Age  int
    City string
}

Your goal is to create a function that takes this slice and groups the people by their city. In other words, you want a map where the keys are city names, and the values are slices of people living in those cities.

The Solution: A Generic Function

With Go 1.18 and the introduction of generics, you can create a generic function to group a slice of structs by a specific property. Here's the code for such a function:

package main

import (
    "fmt"
)

// GroupByProperty groups a slice of structs by a specific property.
func GroupByProperty[T any, K comparable](items []T, getProperty func(T) K) map[K][]T {
    grouped := make(map[K][]T)

    for _, item := range items {
        key := getProperty(item)
        grouped[key] = append(grouped[key], item)
    }

    return grouped
}

Let's break down this code:

  • GroupByProperty is a generic function that takes two parameters:

    • items []T: A slice of any type T.

    • getProperty func(T) K: A function that extracts a property of type K from each element of the slice.

Inside the function:

  • We initialize an empty map called grouped where the keys will be the property values (K), and the values will be slices of elements ([]T) that share the same property value.

  • We loop through the items slice and use the getProperty function to extract the property value (key) for each element. We then append the current element to the corresponding slice in the grouped map.

  • Finally, we return the grouped map containing the grouped elements.

Putting It into Action

Now that we have our generic function, let's use it to group our Person structs by the City property:

func main() {
    people := []Person{
        {Name: "Alice", Age: 25, City: "New York"},
        {Name: "Bob", Age: 30, City: "Los Angeles"},
        {Name: "Charlie", Age: 25, City: "New York"},
        {Name: "David", Age: 35, City: "Chicago"},
    }

    // Group people by the "City" property.
    groupedByCity := GroupByProperty(people, func(p Person) string {
        return p.City
    })

    // Print the grouped data.
    for city, group := range groupedByCity {
        fmt.Printf("City: %s\n", city)
        for _, person := range group {
            fmt.Printf("  Name: %s, Age: %d\n", person.Name, person.Age)
        }
    }
}

In this code:

  • We call GroupByProperty with our people slice and a function that extracts the City property from each Person.

  • The result, groupedByCity, is a map where the keys are city names, and the values are slices of Person structs who have that city as their property value.

  • Finally, we print the grouped data.

Conclusion

Thanks to the introduction of generics in Go 1.18, tasks like grouping a slice of structs by a specific property have become much more elegant and maintainable. The GroupByProperty function we created is a generic solution that can be used to group slices of any type of struct by any property, making your code cleaner and more reusable. As Go continues to evolve, it's exciting to see how generics will simplify and improve various aspects of the language. Happy coding!