Функциональное программирование в Go: от императива к декларативности
Go (Golang) традиционно ассоциируется с императивным и concurrent-программированием, однако его мощная система типов и поддержка функций первого класса делают возможным применение многих принципов функционального программирования (ФП). В этой статье мы разберем, как применять ФП-идиомы в Go, не жертвуя производительностью и читаемостью кода.
Основные принципы ФП в контексте Go
Функциональное программирование в Go не означает полный отказ от изменяемого состояния. Вместо этого мы используем следующие концепции:
- Чистые функции — функции, результат которых зависит только от аргументов, без побочных эффектов.
- Неизменяемость (Immutability) — предпочтение создания новых структур данных вместо изменения существующих.
- Функции высшего порядка — функции, принимающие другие функции как аргументы или возвращающие их.
- Замыкания (Closures) — функции, захватывающие переменные из внешней области видимости.
Функции высшего порядка: map, filter, reduce
В Go нет встроенных дженериков (до версии 1.18), поэтому классические map, filter, reduce реализуются через интерфейсы или явное копирование. Рассмотрим пример с целыми числами:
package main
import "fmt"
// Map — применяет функцию к каждому элементу слайса
func Map(s []int, fn func(int) int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
// Filter — оставляет только элементы, удовлетворяющие условию
func Filter(s []int, fn func(int) bool) []int {
var result []int
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Reduce — сворачивает слайс в одно значение
func Reduce(s []int, init int, fn func(int, int) int) int {
acc := init
for _, v := range s {
acc = fn(acc, v)
}
return acc
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Удваиваем каждый элемент
doubled := Map(nums, func(x int) int { return x * 2 })
fmt.Println("Map:", doubled) // [2 4 6 8 10]
// Оставляем только четные
evens := Filter(nums, func(x int) bool { return x%2 == 0 })
fmt.Println("Filter:", evens) // [2 4]
// Суммируем все элементы
sum := Reduce(nums, 0, func(acc, x int) int { return acc + x })
fmt.Println("Reduce:", sum) // 15
}
Важно: В Go 1.18+ с дженериками можно сделать универсальные версии:
// Generic Map
func MapGeneric[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
Замыкания и каррирование в Go
Замыкания в Go — мощный инструмент для создания функций с сохранением состояния. Рассмотрим пример генератора последовательности:
package main
import "fmt"
// Генератор последовательности Фибоначчи
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
// Каррирование: функция, принимающая аргументы поэтапно
func add(a int) func(int) int {
return func(b int) int {
return a + b
}
}
func main() {
fib := fibonacci()
fmt.Println(fib()) // 1
fmt.Println(fib()) // 1
fmt.Println(fib()) // 2
// Каррирование
add5 := add(5)
fmt.Println(add5(10)) // 15
fmt.Println(add5(20)) // 25
}
Замыкания особенно полезны для:
- Создания конфигурируемых функций (middleware)
- Реализации паттерна "фабрика функций"
- Организации callback-механизмов
Неизменяемость и работа с копиями
Go не поддерживает неизменяемые структуры на уровне языка, но мы можем следовать соглашениям:
package main
import "fmt"
// Неизменяемая структура — методы возвращают новые копии
type Point struct {
X, Y int
}
// WithX — возвращает новую точку с измененным X
func (p Point) WithX(x int) Point {
return Point{X: x, Y: p.Y}
}
// WithY — возвращает новую точку с измененным Y
func (p Point) WithY(y int) Point {
return Point{X: p.X, Y: y}
}
func main() {
p1 := Point{X: 10, Y: 20}
p2 := p1.WithX(30)
fmt.Printf("p1: %+v\n", p1) // {X:10 Y:20}
fmt.Printf("p2: %+v\n", p2) // {X:30 Y:20}
// p1 остался неизменным
}
Совет: Используйте copy() для слайсов и передачу по значению для небольших структур. Для больших данных применяйте интерфейсы, чтобы скрыть мутабельность.
Композиция функций и pipeline
Функциональный pipeline в Go можно организовать через цепочки вызовов:
package main
import (
"fmt"
"strings"
)
// Pipeline функций
type Transform func(string) string
// Compose — композиция двух функций
func Compose(f, g Transform) Transform {
return func(s string) string {
return f(g(s))
}
}
func main() {
toUpper := strings.ToUpper
trim := strings.TrimSpace
addExclamation := func(s string) string { return s + "!" }
// Композиция: trim -> toUpper -> addExclamation
process := Compose(addExclamation, Compose(toUpper, trim))
result := process(" hello world ")
fmt.Println(result) // "HELLO WORLD!"
}
Для более сложных pipeline используйте слайсы функций:
func Pipeline(input string, fns ...Transform) string {
result := input
for _, fn := range fns {
result = fn(result)
}
return result
}
// Использование:
result := Pipeline(" test ", trim, toUpper, addExclamation)
fmt.Println(result) // "TEST!"
Заключение
Функциональное