Grouping a Collection
Grouping means sorting elements into buckets. You choose a key for every element, and all elements with the same key land in the same bucket.
The key is computed by a small function that takes one element and returns its key. For example, the first letter of a word.
Go has no built-in groupBy method on slices. The natural way to group in Go is a map whose keys are the group keys and whose values are slices of elements: map[K][]T. You fill it with a for loop and append(), or you write the small generic helper below once and reuse it.
A map has no fixed order, but fmt.Println() prints a map sorted by key. So the printed output always looks the same.
Generic GroupBy helper syntax
T is the element type and K is the key type. K must be comparable because map keys must support ==. For each element the helper calls key() and appends the element to the slice stored under that key. Appending to a key that does not exist yet just starts a new slice.
func GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T {
groups := make(map[K][]T)
for _, v := range items {
k := key(v)
groups[k] = append(groups[k], v)
}
return groups
}
Group By Key
The simplest grouping keeps the elements as they are and only decides which bucket each one goes to.
Pass the slice and a key function to the GroupBy helper. The result is a map from key to the slice of elements with that key. The elements inside a group keep the order they had in the input.
Sometimes you want to change the elements while grouping, for example store them in upper case. Go has no valueTransform parameter for this. Write the loop yourself: compute the key, compute the new value, and append the new value to the group.
Group words by first letter
The key function takes the first letter of the word with w[:1] and makes it upper case with strings.ToUpper(). Words that start with the same letter end up in the same slice.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
// GroupBy puts every element into the group chosen by key().
func GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T {
groups := make(map[K][]T)
for _, v := range items {
k := key(v)
groups[k] = append(groups[k], v)
}
return groups
}
func main() {
words := []string{"one", "two", "three", "four", "five"}
// The key is the first letter of the word, in upper case.
byFirstLetter := GroupBy(words, func(w string) string { return strings.ToUpper(w[:1]) })
fmt.Println(byFirstLetter)
}
Group and change the values
The key is the lower-case first letter. The stored value is the whole word in upper case. A plain for loop does both steps and appends to groups[key].
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
words := []string{"one", "two", "three", "four", "five"}
groups := make(map[string][]string)
for _, w := range words {
key := w[:1] // lower-case first letter
value := strings.ToUpper(w) // the value stored in the group
groups[key] = append(groups[key], value)
}
fmt.Println(groups)
}
Count Each Group
Often you do not need the elements of each group, only how many there are.
Go has no groupingBy() or eachCount(). Use a map[string]int instead. Reading a key that is not in the map gives 0, so counts[key]++ works even for the first element of a group.
Count words per first letter
Every word adds 1 to the count of its first letter. The map ends up with one number per letter.
Output: Go program and captured output
package main
import "fmt"
func main() {
words := []string{"one", "two", "three", "four", "five"}
// counts[k] starts at 0 for a new key, so ++ works right away.
counts := make(map[string]int)
for _, w := range words {
counts[w[:1]]++
}
fmt.Println(counts)
}
Fold and Reduce per Group
Folding a group means combining its elements into one value, step by step. A running total is the classic example: start at 0 and add each element.
Go has no fold() or aggregate() on groups. The Go way is a map that holds the running value of every group. For each element, compute its key and update the value stored under that key.
When the running value has a starting value, such as 0 for a sum, a missing key already gives you that start. When the first element itself must start the value, check with the value, ok := m[key] pattern whether the key exists yet.
Sum each group
Each number is put in the group "even" or "odd" and added to that group's sum with sums[key] += n. A new key starts at 0, so no special case is needed.
Output: Go program and captured output
package main
import "fmt"
func main() {
numbers := []int{3, 4, 5, 6, 7, 8}
// sums[k] starts at 0 for a new key, so += works right away.
sums := make(map[string]int)
for _, n := range numbers {
key := "odd"
if n%2 == 0 {
key = "even"
}
sums[key] += n
}
fmt.Println(sums)
}
Build a string per group
The first number of a group starts the string. Every later number is joined with "+". The exists flag from chains[key] tells the two cases apart. strconv.Itoa() turns a number into text.
Output: Go program and captured output
package main
import (
"fmt"
"strconv"
)
func main() {
numbers := []int{3, 4, 5, 6, 7, 8}
chains := make(map[string]string)
for _, n := range numbers {
key := "odd"
if n%2 == 0 {
key = "even"
}
// The first element of a group starts the string; later ones are added with "+".
current, exists := chains[key]
if !exists {
chains[key] = strconv.Itoa(n)
} else {
chains[key] = current + "+" + strconv.Itoa(n)
}
}
fmt.Println(chains)
}