Функциональное программирование в Go: от А до Я

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

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

Начать курс

Функциональное программирование в Go: от А до Я

Go (Golang) — язык, который часто ассоциируют с императивным и конкурентным программированием. Однако его гибкая система типов и первоклассные функции позволяют успешно применять функциональный стиль. В этой статье мы разберём, как использовать функциональное программирование (ФП) в Go: от базовых концепций до практических примеров. Вы узнаете, как писать более чистый, предсказуемый и тестируемый код.

Что такое функциональное программирование в контексте Go?

Функциональное программирование — это парадигма, где вычисления строятся на применении функций, избегая изменения состояния и побочных эффектов. В Go это выражается через:

  • Чистые функции — результат зависит только от аргументов, нет мутации внешних данных.
  • Функции высшего порядка — функции, принимающие другие функции как аргументы или возвращающие их.
  • Замыкания — анонимные функции, «запоминающие» переменные из внешней области видимости.
  • Неизменяемость данных — предпочтение копированию, а не изменению.

Хотя Go не является чисто функциональным языком (в нём нет монад или алгебраических типов данных), он предоставляет достаточно инструментов для эффективного использования ФП-подходов.

Анонимные функции и замыкания

В Go функции — это объекты первого класса. Их можно присваивать переменным, передавать в другие функции и возвращать. Анонимные функции (лямбды) — основа функционального стиля.

Пример анонимной функции

package main

import "fmt"

func main() {
    // Анонимная функция, присвоенная переменной
    multiply := func(x, y int) int {
        return x * y
    }

    result := multiply(5, 3)
    fmt.Println(result) // 15
}

Замыкания (Closures)

Замыкание — это анонимная функция, которая «захватывает» переменные из внешней области видимости. Это мощный инструмент для создания генераторов и функций с состоянием.

package main

import "fmt"

// Генератор последовательности
func sequence() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {
    next := sequence()
    fmt.Println(next()) // 1
    fmt.Println(next()) // 2
    fmt.Println(next()) // 3

    // Каждый вызов sequence() создаёт новое замыкание
    next2 := sequence()
    fmt.Println(next2()) // 1
}

Замыкания идеально подходят для инкапсуляции состояния без использования глобальных переменных — это основа функционального подхода в Go.

Чистые функции и неизменяемость

Чистая функция не имеет побочных эффектов и не изменяет переданные данные. В Go это требует сознательного подхода, так как слайсы и мапы передаются по ссылке.

Пример чистой функции (без мутации)

package main

import "fmt"

// Чистая функция: не изменяет исходный слайс
func doubleValues(input []int) []int {
    result := make([]int, len(input))
    for i, v := range input {
        result[i] = v * 2
    }
    return result
}

func main() {
    original := []int{1, 2, 3, 4}
    doubled := doubleValues(original)

    fmt.Println("Original:", original) // [1 2 3 4]
    fmt.Println("Doubled:", doubled)   // [2 4 6 8]
}

Обратите внимание: мы создаём новый слайс вместо изменения исходного. Это обеспечивает неизменяемость (immutability) — ключевой принцип ФП.

Функции высшего порядка: map, filter, reduce

Хотя в Go нет встроенных методов map/filter/reduce для слайсов, их легко реализовать самостоятельно.

package main

import "fmt"

// Map — применяет функцию к каждому элементу
func Map[T any](items []T, fn func(T) T) []T {
    result := make([]T, len(items))
    for i, v := range items {
        result[i] = fn(v)
    }
    return result
}

// Filter — оставляет элементы, удовлетворяющие условию
func Filter[T any](items []T, fn func(T) bool) []T {
    var result []T
    for _, v := range items {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce — свёртка слайса в одно значение
func Reduce[T any, R any](items []T, initial R, fn func(R, T) R) R {
    result := initial
    for _, v := range items {
        result = fn(result, v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}

    // Удвоить все чётные числа
    evenDoubled := Map(
        Filter(numbers, func(n int) bool { return n%2 == 0 }),
        func(n int) int { return n * 2 },
    )
    fmt.Println(evenDoubled) // [4 8 12]

    // Сумма квадратов всех чисел
    sumSquares := Reduce(numbers, 0, func(acc int, n int) int {
        return acc + n*n
    })
    fmt.Println(sumSquares) // 91
}

Используя дженерики (Go 1.18+), мы создали универсальные функции, которые работают с любыми типами данных. Это делает код переиспользуемым и декларативным.

Практические паттерны функционального программирования в Go

Рассмотрим несколько реальных сценариев, где ФП-подходы особенно полезны.

1. Обработка ошибок через функциональные опции

Паттерн «Functional Options» (Роб Пайк) — элегантная альтернатива множеству конструкторов и параметрам.

package main

import "fmt"

type Server struct {
    host string
    port int
    tls  bool
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTLS(tls bool) Option {
    return func(s *Server) {
        s.tls = tls
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080, tls: false}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func main() {
    server := NewServer(WithHost("example                

Блоги

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