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

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

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

Начать курс

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

Блоги

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