В работе с коллекциями часто повторяется одна и та же логика: преобразовать все элементы, оставить только подходящие или свести список к одному значению. В других языках программирования для этого существуют стандартные функции map
, filter
и reduce
. В Go их можно реализовать самостоятельно, а благодаря дженерикам — так, чтобы они работали с любыми типами.
Функция отображения Map
получает срез элементов и функцию-преобразователь. Она создаёт новый срез, куда кладёт результат работы преобразователя для каждого элемента. Параметры типа T
и R
задают тип входных и выходных значений.
func Map[T, R any](in []T, fn func(T) R) []R {
out := make([]R, len(in))
for i, v := range in {
out[i] = fn(v)
}
return out
}
Если вызвать её так:
nums := []int{1, 2, 3}
squared := Map(nums, func(x int) int { return x * x })
// [1 4 9]
То каждый элемент будет возведён в квадрат. Та же функция работает и для строк, если преобразователь возвращает, например, длину:
words := []string{"go", "generics", "map"}
lengths := Map(words, func(s string) int { return len(s) })
// [2 8 3]
Функция фильтрации Filter
оставляет только те элементы, которые удовлетворяют условию. В сигнатуре параметр типа один, так как на входе и выходе остаются те же элементы.
func Filter[T any](in []T, pred func(T) bool) []T {
out := make([]T, 0, len(in))
for _, v := range in {
if pred(v) {
out = append(out, v)
}
}
return out
}
Пример использования:
nums := []int{1, 2, 3, 4, 5, 6}
evens := Filter(nums, func(x int) bool { return x%2 == 0 })
// [2 4 6]
Та же логика работает и для строк:
words := []string{"go", "java", "python", "c"}
long := Filter(words, func(s string) bool { return len(s) > 2 })
// ["java" "python"]
Функция свёртки Reduce
объединяет элементы среза в одно значение. Она принимает аккумулятор начального значения и функцию объединения. Тип аккумулятора R
может совпадать с типом элементов или отличаться от него.
func Reduce[T, R any](in []T, init R, combine func(R, T) R) R {
acc := init
for _, v := range in {
acc = combine(acc, v)
}
return acc
}
Примеры использования:
nums := []int{1, 2, 3, 4}
sum := Reduce(nums, 0, func(acc, x int) int { return acc + x })
// 10
words := []string{"Go", "is", "fun"}
sentence := Reduce(words, "", func(acc, s string) string {
if acc == "" {
return s
}
return acc + " " + s
})
// "Go is fun"
Такие обобщённые функции для коллекций дают сразу несколько преимуществ. Они устраняют дублирование кода, делают его выразительным и лаконичным. Строгая типизация сохраняется: компилятор знает, какие типы подставлены, и не позволит вызвать функцию с неподходящими аргументами. Производительность остаётся на уровне обычных циклов, потому что внутри нет приведения к interface{}
.
Особую роль здесь играют замыкания. В примерах выше условия и преобразования описываются как функции, которые можно создавать прямо в месте вызова. Эти функции замыкают переменные внешнего окружения. Например:
threshold := 10
greater := Filter(nums, func(x int) bool { return x > threshold })
// оставляет числа больше 10
Переменная threshold
не передаётся в сигнатуре, но доступна внутри функции-предиката. Это делает код компактным и удобным: параметры типа описывают только структуру данных, а детали логики остаются на стороне замыканий.
В результате обобщённые реализации Map
, Filter
и Reduce
становятся строительными блоками для работы с любыми коллекциями. Их можно комбинировать: сначала отфильтровать данные, затем преобразовать и в конце свести к одному результату. Всё это выполняется без дублирования, с полным контролем типов и в привычном для Go стиле.
Самостоятельная работа
Реализуем обобщённые функции высшего порядка.
- Реализуйте
Any[T any](in []T, pred func(T) bool) bool
— возвращаетtrue
, если хотя бы один элемент удовлетворяет предикату. - Реализуйте
Count[T any](in []T, pred func(T) bool) int
— возвращает количество элементов, удовлетворяющих предикату.
Проверьте на срезе чисел и строк (например, «есть ли чётные», «сколько строк длиннее 3»).
nums := []int{1,2,3,4,5,6}
fmt.Println(Any(nums, func(x int) bool { return x%2==0 })) // true
fmt.Println(Count(nums, func(x int) bool { return x%2==0 })) // 3
words := []string{"go","gen","map","filters"}
fmt.Println(Any(words, func(s string) bool { return len(s) > 3 })) // true
fmt.Println(Count(words, func(s string) bool { return len(s) > 3 })) // 2
Показать решение
package main
import "fmt"
func Any[T any](in []T, pred func(T) bool) bool {
for _, v := range in {
if pred(v) {
return true
}
}
return false
}
func Count[T any](in []T, pred func(T) bool) int {
c := 0
for _, v := range in {
if pred(v) {
c++
}
}
return c
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
fmt.Println(Any(nums, func(x int) bool { return x%2 == 0 })) // true
fmt.Println(Count(nums, func(x int) bool { return x%2 == 0 })) // 3
words := []string{"go", "gen", "map", "filters"}
fmt.Println(Any(words, func(s string) bool { return len(s) > 3 })) // true
fmt.Println(Count(words, func(s string) bool { return len(s) > 3 })) // 2
}
Дополнительные материалы
- Wikipedia — Map (higher-order function)
- Wikipedia — Filter (higher-order function)
- Wikipedia — Fold/Reduce (higher-order function)
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.