Функциональное программирование в Go: от теории к практике
Go (Golang) изначально не был задуман как чисто функциональный язык. Однако его мощная система типов, поддержка функций первого класса и замыканий позволяют с успехом применять принципы функционального программирования (ФП). В этой статье мы разберем, как использовать функциональный стиль в Go для написания более предсказуемого, тестируемого и параллельно безопасного кода.
1. Функции первого класса и функции высшего порядка
В Go функции являются объектами первого класса. Это означает, что их можно присваивать переменным, передавать как аргументы и возвращать из других функций. Функции, которые работают с другими функциями, называются функциями высшего порядка.
Пример: передача функции как аргумента
package main
import "fmt"
// Функция высшего порядка: принимает функцию и применяет её к числу
func applyOperation(x int, op func(int) int) int {
return op(x)
}
func double(x int) int {
return x * 2
}
func square(x int) int {
return x * x
}
func main() {
result1 := applyOperation(5, double)
result2 := applyOperation(5, square)
fmt.Println("Double:", result1) // 10
fmt.Println("Square:", result2) // 25
}
Пример: возврат функции из функции (фабрика функций)
package main
import "fmt"
// Возвращает функцию, которая добавляет заданное значение
func createAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
addFive := createAdder(5)
addTen := createAdder(10)
fmt.Println(addFive(3)) // 8
fmt.Println(addTen(3)) // 13
}
2. Замыкания (Closures) в Go
Замыкание — это функция, которая "запоминает" переменные из внешней области видимости, даже после того, как эта область завершила выполнение. Замыкания — мощный инструмент для создания инкапсулированного состояния и фабрик функций.
Пример: генератор последовательности
package main
import "fmt"
func sequenceGenerator() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
next := sequenceGenerator()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
// Новый генератор начинается с 1
next2 := sequenceGenerator()
fmt.Println(next2()) // 1
}
Важно: Будьте осторожны с замыканиями в циклах. В старых версиях Go (до 1.22) переменная i в цикле переиспользовалась, что приводило к ошибкам. Начиная с Go 1.22, переменные цикла захватываются по значению.
3. Чистые функции и иммутабельность
Чистая функция — это функция, которая:
- Всегда возвращает одинаковый результат для одинаковых аргументов.
- Не имеет побочных эффектов (не изменяет внешнее состояние, не пишет в файлы/сеть).
В Go нет встроенной поддержки иммутабельности, но мы можем следовать этому принципу самостоятельно.
Пример: чистая vs нечистая функция
package main
import "fmt"
// НЕЧИСТАЯ функция: изменяет внешний слайс
var counter int
func impureAddToSlice(s []int, val int) []int {
counter++ // побочный эффект
return append(s, val)
}
// ЧИСТАЯ функция: создаёт новый слайс
func pureAddToSlice(s []int, val int) []int {
newSlice := make([]int, len(s)+1)
copy(newSlice, s)
newSlice[len(s)] = val
return newSlice
}
func main() {
original := []int{1, 2, 3}
// Чистый подход
result := pureAddToSlice(original, 4)
fmt.Println("Original:", original) // [1 2 3] - не изменился
fmt.Println("Result:", result) // [1 2 3 4]
}
Совет: Для работы с иммутабельными структурами данных в Go часто используют копирование слайсов и мап, либо библиотеки вроде github.com/arr-ai/frozen.
4. Реализация map, filter и reduce в Go
В Go нет встроенных дженериков для map/filter/reduce до версии 1.18. Однако с появлением дженериков мы можем написать универсальные утилиты. До Go 1.18 приходилось писать отдельные функции для каждого типа.
Пример: дженерик-версии map и filter (Go 1.18+)
package main
import "fmt"
// Map: применяет функцию к каждому элементу слайса
func Map[T, U any](input []T, fn func(T) U) []U {
result := make([]U, len(input))
for i, v := range input {
result[i] = fn(v)
}
return result
}
// Filter: возвращает только элементы, удовлетворяющие условию
func Filter[T any](input []T, fn func(T) bool) []T {
var result []T
for _, v := range input {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Reduce: свёртка слайса в одно значение
func Reduce[T, U any](input []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range input {
result = fn(result, v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
// Map: удвоить числа
doubled := Map(numbers, func(x int) int { return x * 2 })
fmt.Println("Doubled:", doubled) // [2 4 6 8 10 12]
// Filter: оставить только чётные
evens := Filter(numbers, func(x int) bool { return x%2 == 0 })
fmt.Println("Evens:", evens) // [2 4 6]
// Reduce: сумма всех чисел
sum := Reduce(numbers, 0, func(acc, x int) int { return acc + x })
fmt.Println("Sum:", sum) // 21
}
Важное замечание: В Go принято писать итерации явно (циклы for range), так как это делает код более читаемым для большинства Go-разработчиков. Используйте map/filter/reduce, когда это действительно упрощает код, а не ради "функционального стиля".
5. Практические советы по функциональному Go
- Не злоупотребляйте. Go — мультипарадигмен