Операции агрегирования
Операция агрегирования превращает всю коллекцию в одно значение. Примеры: количество элементов, их сумма, самый большой элемент или все элементы, соединённые в один результат.
Некоторые из них Go даёт сразу. len() считает элементы. slices.Min() и slices.Max() из пакета slices находят самый маленький и самый большой элемент. Для суммы или среднего встроенной функции нет: элементы складывают в цикле for.
Будьте внимательны: slices.Min() и slices.Max() вызывают панику (программа аварийно завершается) на пустом slice. Сначала проверяйте len(s) > 0, если slice может быть пустым.
Во многих языках есть методы fold и reduce, которые соединяют элементы один за другим. В Go таких методов у slice нет. Вместо этого вы один раз пишете небольшой обобщённый helper и потом используете его повторно. Helper Fold ниже работает со slice любого типа.
Синтаксис обобщённого helper Fold
T — это тип элемента, а R — тип результата. acc — это аккумулятор: он начинается со значения initial, а f по очереди соединяет его с каждым элементом. Последнее значение acc — это результат.
func Fold[T, R any](items []T, initial R, f func(R, T) R) R {
acc := initial
for _, v := range items {
acc = f(acc, v)
}
return acc
}
Основные агрегаты
Самые частые агрегаты — это количество, минимум, максимум, сумма и среднее.
len(numbers)даёт количество.slices.Min(numbers)иslices.Max(numbers)дают самый маленький и самый большой элемент. Они работают с числами и со строками.- Сумма — это цикл, который прибавляет каждый элемент к переменной. Среднее — это сумма, делённая на количество. Сначала приведите оба значения к
float64, иначе деление отбросит дробную часть.
Иногда самый маленький или самый большой элемент нужно выбрать по правилу, а не по самому значению. slices.MinFunc() и slices.MaxFunc() принимают функцию сравнения. Функция получает два элемента и возвращает отрицательное число, ноль или положительное число. cmp.Compare() из пакета cmp напишет это за вас.
Иногда каждый элемент нужно сначала преобразовать функцией, а результаты сложить. Встроенной функции для этого в Go нет, поэтому в примере используется небольшой обобщённый helper SumOf.
Количество, минимум, максимум, сумма и среднее
Для чисел 6, 42, 10 и 4 выведите количество с помощью len(), самый большой и самый маленький элемент с помощью slices.Max() и slices.Min(), а сумму — из цикла. Среднее равно 62 / 4 = 15.5.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{6, 42, 10, 4}
// В Go нет функции Sum(): складываем элементы в цикле.
sum := 0
for _, n := range numbers {
sum += n
}
fmt.Println("Количество:", len(numbers))
fmt.Println("Максимум:", slices.Max(numbers))
fmt.Println("Минимум:", slices.Min(numbers))
fmt.Println("Среднее:", float64(sum)/float64(len(numbers)))
fmt.Println("Сумма:", sum)
}
Минимум и максимум по правилу
slices.MinFunc() сравнивает остатки a%3 и b%3. Остатки чисел 5, 42, 10 и 4 равны 2, 0, 1 и 1, поэтому побеждает 42. slices.MaxFunc() сравнивает длины строк и находит "three".
Вывод: программа на Go и захваченный вывод
package main
import (
"cmp"
"fmt"
"slices"
)
func main() {
numbers := []int{5, 42, 10, 4}
strings := []string{"one", "two", "three", "four"}
// Элемент, у которого остаток от деления на 3 самый маленький.
minRem := slices.MinFunc(numbers, func(a, b int) int {
return cmp.Compare(a%3, b%3)
})
fmt.Println(minRem)
// Самая длинная строка, сравнение по длине.
longest := slices.MaxFunc(strings, func(a, b string) int {
return cmp.Compare(len(a), len(b))
})
fmt.Println(longest)
}
Сумма функции от каждого элемента
SumOf применяет функцию к каждому элементу и складывает результаты. Удвоение 5, 42, 10 и 4 даёт 122. Деление их пополам как float даёт 30.5.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Number — это любой тип, который можно складывать с помощью +.
type Number interface {
~int | ~float64
}
// SumOf превращает каждый элемент в число с помощью f и складывает результаты.
func SumOf[T any, N Number](items []T, f func(T) N) N {
var total N
for _, v := range items {
total += f(v)
}
return total
}
func main() {
numbers := []int{5, 42, 10, 4}
fmt.Println(SumOf(numbers, func(n int) int { return n * 2 }))
fmt.Println(SumOf(numbers, func(n int) float64 { return float64(n) / 2 }))
}
Fold и Reduce
Fold и Reduce шаг за шагом соединяют все элементы в одно значение. Они хранят текущее значение, которое называется аккумулятор. Для каждого элемента они вызывают функцию с аккумулятором и элементом, а функция возвращает новый аккумулятор.
Разница — в начальном значении:
Foldначинает с начального значения, которое вы ему передаёте. Функция выполняется один раз для каждого элемента.Reduceначинает с первого элемента. Функция выполняется начиная со второго элемента, поэтому первый элемент через неё никогда не проходит.
Это важно, когда функция изменяет элемент. Если она удваивает каждый элемент, Reduce оставляет первый элемент без изменений.
В Go нет ни встроенного fold, ни встроенного reduce. Оба — это небольшие обобщённые helper с циклом for внутри. Чтобы свернуть справа, цикл просто начинается с последнего индекса и идёт вниз до 0.
Reduce
Reduce берёт 5 за начало и прибавляет к нему 2, 10 и 4. Результат — 21.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Reduce начинает с первого элемента и соединяет с ним остальные.
func Reduce[T any](items []T, f func(T, T) T) T {
acc := items[0]
for _, v := range items[1:] {
acc = f(acc, v)
}
return acc
}
func main() {
numbers := []int{5, 2, 10, 4}
sum := Reduce(numbers, func(sum, element int) int { return sum + element })
fmt.Println(sum)
}
Fold
Fold начинает с 0 и прибавляет каждый элемент, умноженный на 2. Каждый элемент удваивается: 10 + 4 + 20 + 8 = 42.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Fold начинает с начального значения и соединяет с ним каждый элемент.
func Fold[T, R any](items []T, initial R, f func(R, T) R) R {
acc := initial
for _, v := range items {
acc = f(acc, v)
}
return acc
}
func main() {
numbers := []int{5, 2, 10, 4}
sumDoubled := Fold(numbers, 0, func(sum, element int) int { return sum + element*2 })
fmt.Println(sumDoubled)
}
Fold против Reduce
Оба helper используют одну и ту же функцию: сумма плюс элемент, умноженный на 2. Reduce даёт 37, потому что первый элемент 5 является началом и не удваивается. Fold с начальным значением 0 даёт 42.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Reduce берёт за начало первый элемент, поэтому f к нему никогда не применяется.
func Reduce[T any](items []T, f func(T, T) T) T {
acc := items[0]
for _, v := range items[1:] {
acc = f(acc, v)
}
return acc
}
// Fold берёт за начало initial, поэтому f применяется к каждому элементу.
func Fold[T, R any](items []T, initial R, f func(R, T) R) R {
acc := initial
for _, v := range items {
acc = f(acc, v)
}
return acc
}
func main() {
numbers := []int{5, 2, 10, 4}
// Здесь 5 — начальное значение, поэтому оно не удваивается: 5 + 4 + 20 + 8
reduced := Reduce(numbers, func(sum, element int) int { return sum + element*2 })
fmt.Println("Reduce:", reduced)
// Каждый элемент удваивается: 10 + 4 + 20 + 8
folded := Fold(numbers, 0, func(sum, element int) int { return sum + element*2 })
fmt.Println("Fold:", folded)
}
Свёртка справа
FoldRight идёт по slice от последнего элемента к первому. Для сложения порядок не важен, поэтому результат снова 42.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// FoldRight работает как Fold, но идёт по slice от последнего элемента к первому.
func FoldRight[T, R any](items []T, initial R, f func(T, R) R) R {
acc := initial
for i := len(items) - 1; i >= 0; i-- {
acc = f(items[i], acc)
}
return acc
}
func main() {
numbers := []int{5, 2, 10, 4}
sumDoubledRight := FoldRight(numbers, 0, func(element, sum int) int { return sum + element*2 })
fmt.Println(sumDoubledRight)
}
Fold с индексом
Иногда важна позиция элемента, а не только его значение.
В Go обычный цикл уже даёт вам индекс. for idx, element := range numbers идёт с начала. for idx := len(numbers) - 1; idx >= 0; idx-- идёт с конца. Внутри цикла if проверяет индекс и решает, прибавлять ли элемент.
Fold с индексом
Складываются только элементы с чётными индексами: в индексе 0 находится 5, а в индексе 2 — 10, поэтому сумма равна 15.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []int{5, 2, 10, 4}
// range даёт ещё и индекс, поэтому условие может его использовать.
sumEven := 0
for idx, element := range numbers {
if idx%2 == 0 {
sumEven += element
}
}
fmt.Println(sumEven)
}
Fold с индексом справа
То же правило, но цикл начинается с индекса 3 и идёт вниз до 0. Результат всё тот же — 15.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []int{5, 2, 10, 4}
// Идём от последнего индекса вниз до 0 и складываем элементы с чётными индексами.
sumEven := 0
for idx := len(numbers) - 1; idx >= 0; idx-- {
if idx%2 == 0 {
sumEven += numbers[idx]
}
}
fmt.Println(sumEven)
}
Безопасный Reduce
Reduce нужен первый элемент, чтобы с него начать. На пустом slice items[0] вызывает панику, и программа аварийно завершается.
В Go для чисел нет null. Способ Go — возвращать два значения: результат и bool с именем ok. Когда slice пустой, ok равен false, а результат — это нулевое значение типа, например 0 для int. Это тот же шаблон value, ok, который вы знаете по map.
Reduce со значением ok
ReduceOK возвращает 21 true для чисел 5, 2, 10 и 4 и 0 false для пустого slice.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// ReduceOK возвращает ok = false вместо паники на пустом slice.
func ReduceOK[T any](items []T, f func(T, T) T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
acc := items[0]
for _, v := range items[1:] {
acc = f(acc, v)
}
return acc, true
}
func main() {
numbers := []int{5, 2, 10, 4}
empty := []int{}
sum, ok := ReduceOK(numbers, func(sum, element int) int { return sum + element })
fmt.Println(sum, ok)
sum, ok = ReduceOK(empty, func(sum, element int) int { return sum + element })
fmt.Println(sum, ok)
}
Накопительные Fold и Reduce
Накопительный fold или reduce не выбрасывает промежуточные шаги. Он сохраняет аккумулятор после каждого элемента, поэтому результат — это slice со всеми промежуточными значениями.
В Go вы держите переменную с суммой и внутри цикла добавляете её в результирующий slice с помощью append(). У накопительного reduce результат начинается с первого элемента. У накопительного fold результат начинается с начального значения.
Накопительный reduce
Числа от 0 до 5 складываются одно за другим. Результирующий slice содержит каждую промежуточную сумму: [0 1 3 6 10 15].
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5}
// Первый элемент — начало; каждый следующий шаг сохраняется в результате.
sum := numbers[0]
runningReduceSum := []int{sum}
for _, item := range numbers[1:] {
sum += item
runningReduceSum = append(runningReduceSum, sum)
}
fmt.Println(runningReduceSum)
}
Накопительный fold
То же самое с начальным значением 10. Результат начинается с самой десятки и содержит на один элемент больше, чем входные данные: [10 10 11 13 16 20 25].
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5}
// Начальное значение 10 — это первый элемент результата.
sum := 10
runningFoldSum := []int{sum}
for _, item := range numbers {
sum += item
runningFoldSum = append(runningFoldSum, sum)
}
fmt.Println(runningFoldSum)
}