Функциональное программирование в Go: принципы и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в 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!"

Заключение

Функциональное

Блоги

Рекомендаций по книгам