Группировка коллекции
Группировка — это распределение элементов по корзинам. Вы выбираете ключ для каждого элемента, и все элементы с одинаковым ключом попадают в одну корзину.
Ключ вычисляет небольшая функция, которая берёт один элемент и возвращает его ключ. Например, первую букву слова.
В Go нет встроенного метода groupBy у слайсов. Естественный способ группировки в Go — это map, ключами которого являются ключи групп, а значениями — слайсы элементов: map[K][]T. Вы заполняете его циклом for и функцией append() или один раз пишете небольшой обобщённый помощник, показанный ниже, и используете его повторно.
У map нет фиксированного порядка, но fmt.Println() печатает map, отсортированный по ключу. Поэтому выведенный результат всегда выглядит одинаково.
Синтаксис обобщённого помощника GroupBy
T — это тип элемента, а K — тип ключа. K должен быть comparable, потому что ключи map должны поддерживать ==. Для каждого элемента помощник вызывает 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
}
Группировка по ключу
Самая простая группировка оставляет элементы такими, какие они есть, и только решает, в какую корзину попадёт каждый из них.
Передайте слайс и функцию ключа помощнику GroupBy. Результат — это map из ключа в слайс элементов с этим ключом. Внутри группы элементы сохраняют тот порядок, который был во входных данных.
Иногда нужно изменить элементы прямо во время группировки, например сохранить их в верхнем регистре. В Go нет параметра valueTransform для этого. Напишите цикл сами: вычислите ключ, вычислите новое значение и добавьте новое значение в группу.
Группировка слов по первой букве
Функция ключа берёт первую букву слова с помощью w[:1] и переводит её в верхний регистр функцией strings.ToUpper(). Слова, начинающиеся с одной и той же буквы, оказываются в одном слайсе.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
// GroupBy помещает каждый элемент в группу, выбранную функцией 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"}
// Ключ — это первая буква слова в верхнем регистре.
byFirstLetter := GroupBy(words, func(w string) string { return strings.ToUpper(w[:1]) })
fmt.Println(byFirstLetter)
}
Группировка с изменением значений
Ключ — это первая буква в нижнем регистре. Сохраняемое значение — это всё слово в верхнем регистре. Обычный цикл for делает оба шага и добавляет значение в groups[key].
Вывод: программа на Go и захваченный вывод
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] // первая буква в нижнем регистре
value := strings.ToUpper(w) // значение, которое сохраняется в группе
groups[key] = append(groups[key], value)
}
fmt.Println(groups)
}
Подсчёт в каждой группе
Часто вам нужны не сами элементы каждой группы, а только их количество.
В Go нет groupingBy() или eachCount(). Вместо этого используйте map[string]int. Чтение ключа, которого нет в map, даёт 0, поэтому counts[key]++ работает даже для первого элемента группы.
Подсчёт слов по первой букве
Каждое слово прибавляет 1 к счётчику своей первой буквы. В итоге в map оказывается по одному числу для каждой буквы.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
words := []string{"one", "two", "three", "four", "five"}
// counts[k] для нового ключа начинается с 0, поэтому ++ работает сразу.
counts := make(map[string]int)
for _, w := range words {
counts[w[:1]]++
}
fmt.Println(counts)
}
Свёртка (fold и reduce) по группам
Свернуть группу — значит шаг за шагом объединить её элементы в одно значение. Классический пример — накопительная сумма: начните с 0 и прибавляйте каждый элемент.
В Go нет fold() или aggregate() для групп. Способ Go — это map, который хранит текущее значение каждой группы. Для каждого элемента вычислите его ключ и обновите значение, сохранённое под этим ключом.
Когда у текущего значения есть начальное значение, например 0 для суммы, отсутствующий ключ уже даёт вам это начало. Когда значение должно начинаться с самого первого элемента, проверьте с помощью шаблона value, ok := m[key], существует ли уже такой ключ.
Сумма каждой группы
Каждое число попадает в группу "чётные" или "нечётные" и прибавляется к сумме этой группы с помощью sums[key] += n. Новый ключ начинается с 0, поэтому особый случай не нужен.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []int{3, 4, 5, 6, 7, 8}
// sums[k] для нового ключа начинается с 0, поэтому += работает сразу.
sums := make(map[string]int)
for _, n := range numbers {
key := "нечётные"
if n%2 == 0 {
key = "чётные"
}
sums[key] += n
}
fmt.Println(sums)
}
Сборка строки для каждой группы
Первое число группы начинает строку. Каждое следующее число присоединяется через "+". Флаг exists из chains[key] различает эти два случая. strconv.Itoa() превращает число в текст.
Вывод: программа на Go и захваченный вывод
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 := "нечётные"
if n%2 == 0 {
key = "чётные"
}
// Первый элемент группы начинает строку; следующие добавляются через "+".
current, exists := chains[key]
if !exists {
chains[key] = strconv.Itoa(n)
} else {
chains[key] = current + "+" + strconv.Itoa(n)
}
}
fmt.Println(chains)
}