Aggregate Operations
An aggregate operation turns a whole collection into one value. Examples are the number of elements, their sum, the biggest element, or all elements combined into one result.
Go gives you some of these directly. len() counts the elements. slices.Min() and slices.Max() from the slices package find the smallest and the biggest element. For a sum or an average there is no built-in function: you add the elements up in a for loop.
Be careful: slices.Min() and slices.Max() panic (crash the program) on an empty slice. Check len(s) > 0 first when the slice might be empty.
Many languages have fold and reduce methods for combining elements one by one. Go has no such methods on slices. Instead you write a small generic helper once and reuse it. The Fold helper below works for a slice of any type.
Generic Fold helper syntax
T is the element type and R is the result type. acc is the accumulator: it starts as initial, and f combines it with every element in turn. The last value of acc is the result.
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
}
Basic Aggregates
The most common aggregates are count, minimum, maximum, sum and average.
len(numbers)gives the count.slices.Min(numbers)andslices.Max(numbers)give the smallest and the biggest element. They work for numbers and strings.- The sum is a loop that adds every element to a variable. The average is the sum divided by the count. Convert both to
float64first, or the division throws away the decimal part.
Sometimes the smallest or biggest element should be picked by a rule, not by the value itself. slices.MinFunc() and slices.MaxFunc() take a compare function. The function gets two elements and returns a negative number, zero or a positive number. cmp.Compare() from the cmp package writes that for you.
Sometimes every element must be converted with a function first and the results added up. Go has no built-in function for that, so the example uses a small generic SumOf helper.
Count, min, max, sum and average
For the numbers 6, 42, 10 and 4 print the count with len(), the biggest and smallest element with slices.Max() and slices.Min(), and the sum from a loop. The average is 62 / 4 = 15.5.
Output: Go program and captured output
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{6, 42, 10, 4}
// Go has no Sum(): add the elements up in a loop.
sum := 0
for _, n := range numbers {
sum += n
}
fmt.Println("Count:", len(numbers))
fmt.Println("Max:", slices.Max(numbers))
fmt.Println("Min:", slices.Min(numbers))
fmt.Println("Average:", float64(sum)/float64(len(numbers)))
fmt.Println("Sum:", sum)
}
Min and max by a rule
slices.MinFunc() compares the remainders a%3 and b%3. The remainders of 5, 42, 10 and 4 are 2, 0, 1 and 1, so 42 wins. slices.MaxFunc() compares string lengths and finds "three".
Output: Go program and captured output
package main
import (
"cmp"
"fmt"
"slices"
)
func main() {
numbers := []int{5, 42, 10, 4}
strings := []string{"one", "two", "three", "four"}
// The element whose remainder after dividing by 3 is the smallest.
minRem := slices.MinFunc(numbers, func(a, b int) int {
return cmp.Compare(a%3, b%3)
})
fmt.Println(minRem)
// The longest string, compared by length.
longest := slices.MaxFunc(strings, func(a, b string) int {
return cmp.Compare(len(a), len(b))
})
fmt.Println(longest)
}
Sum of a function of each element
SumOf applies the function to every element and adds the results. Doubling 5, 42, 10 and 4 gives 122. Halving them as floats gives 30.5.
Output: Go program and captured output
package main
import "fmt"
// Number is any type that can be added with +.
type Number interface {
~int | ~float64
}
// SumOf turns every element into a number with f and adds the results up.
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 and Reduce
Fold and Reduce combine all elements into one value, step by step. They keep a running value called the accumulator. For each element they call a function with the accumulator and the element, and the function returns the new accumulator.
The difference is the starting value:
Foldstarts with an initial value that you give it. The function runs once for every element.Reducestarts with the first element. The function runs for the second element onward, so the first element is never passed through it.
This matters when the function changes the element. If it doubles every element, Reduce leaves the first element as it is.
Go has neither fold nor reduce built in. Both are small generic helpers with a for loop inside. To fold from the right, the loop simply starts at the last index and goes down to 0.
Reduce
Reduce takes 5 as the start and adds 2, 10 and 4 to it. The result is 21.
Output: Go program and captured output
package main
import "fmt"
// Reduce starts with the first element and combines the rest into it.
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 starts with 0 and adds each element times 2. Every element is doubled: 10 + 4 + 20 + 8 = 42.
Output: Go program and captured output
package main
import "fmt"
// Fold starts with an initial value and combines every element into it.
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 versus Reduce
Both helpers use the same function, sum plus element times 2. Reduce gives 37 because the first element 5 is the start and is not doubled. Fold with initial value 0 gives 42.
Output: Go program and captured output
package main
import "fmt"
// Reduce uses the first element as the start, so f is never applied to it.
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 uses initial as the start, so f is applied to every element.
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 is the starting value here, so it is not doubled: 5 + 4 + 20 + 8
reduced := Reduce(numbers, func(sum, element int) int { return sum + element*2 })
fmt.Println("Reduce:", reduced)
// Every element is doubled: 10 + 4 + 20 + 8
folded := Fold(numbers, 0, func(sum, element int) int { return sum + element*2 })
fmt.Println("Fold:", folded)
}
Fold from the right
FoldRight walks the slice from the last element to the first. Addition does not care about the order, so the result is again 42.
Output: Go program and captured output
package main
import "fmt"
// FoldRight is like Fold, but it walks the slice from the last element to the first.
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 with Index
Sometimes the position of an element matters, not only its value.
In Go a plain loop already gives you the index. for idx, element := range numbers walks from the start. for idx := len(numbers) - 1; idx >= 0; idx-- walks from the end. Inside the loop an if checks the index and decides whether to add the element.
Fold with index
Only the elements at even indexes are added: index 0 holds 5 and index 2 holds 10, so the sum is 15.
Output: Go program and captured output
package main
import "fmt"
func main() {
numbers := []int{5, 2, 10, 4}
// range gives the index too, so the condition can use it.
sumEven := 0
for idx, element := range numbers {
if idx%2 == 0 {
sumEven += element
}
}
fmt.Println(sumEven)
}
Fold with index from the right
The same rule, but the loop starts at index 3 and goes down to 0. The result is still 15.
Output: Go program and captured output
package main
import "fmt"
func main() {
numbers := []int{5, 2, 10, 4}
// Walk from the last index down to 0 and add the elements at even indexes.
sumEven := 0
for idx := len(numbers) - 1; idx >= 0; idx-- {
if idx%2 == 0 {
sumEven += numbers[idx]
}
}
fmt.Println(sumEven)
}
Safe Reduce
Reduce needs a first element to start with. On an empty slice items[0] panics and the program crashes.
Go has no null for numbers. The Go way is to return two values: the result and a bool named ok. When the slice is empty, ok is false and the result is the zero value of the type, for example 0 for int. This is the same value, ok pattern you know from maps.
Reduce with an ok value
ReduceOK returns 21 true for the numbers 5, 2, 10 and 4, and 0 false for an empty slice.
Output: Go program and captured output
package main
import "fmt"
// ReduceOK returns ok = false instead of panicking on an empty 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)
}
Running Fold and Reduce
A running fold or reduce does not throw away the steps in between. It saves the accumulator after every element, so the result is a slice of all the running values.
In Go you keep a sum variable and append() it to a result slice inside the loop. For a running reduce the result starts with the first element. For a running fold the result starts with the initial value.
Running reduce
The numbers 0 to 5 are added one by one. The result slice holds every sum along the way: [0 1 3 6 10 15].
Output: Go program and captured output
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5}
// The first element is the start; every later step is saved in the result.
sum := numbers[0]
runningReduceSum := []int{sum}
for _, item := range numbers[1:] {
sum += item
runningReduceSum = append(runningReduceSum, sum)
}
fmt.Println(runningReduceSum)
}
Running fold
The same with the initial value 10. The result begins with 10 itself and has one more element than the input: [10 10 11 13 16 20 25].
Output: Go program and captured output
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5}
// The initial value 10 is the first element of the result.
sum := 10
runningFoldSum := []int{sum}
for _, item := range numbers {
sum += item
runningFoldSum = append(runningFoldSum, sum)
}
fmt.Println(runningFoldSum)
}