Зарегистрируйтесь, чтобы продолжить обучение

Функции высшего порядка Go: Функции

Когда мы только начинаем писать код, функции кажутся чем-то простым: есть входные данные, есть результат. Но чем больше задач мы решаем, тем чаще повторяется одна и та же схема — меняется только маленькая деталь. Чтобы не копировать код, в Go можно использовать функции высшего порядка.

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

Функции как аргументы

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

package main

import "fmt"

// applyToEach применяет функцию f к каждому элементу среза
func applyToEach(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, v := range nums {
        result[i] = f(v) // применяем функцию f
    }
    return result
}

func double(x int) int {
    return x * 2
}

func square(x int) int {
    return x * x
}

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

    doubled := applyToEach(numbers, double)
    squared := applyToEach(numbers, square)

    fmt.Println("Исходные:", numbers)
    fmt.Println("Удвоенные:", doubled)
    fmt.Println("Возведённые в квадрат:", squared)
}

Функция applyToEach — это функция высшего порядка. Она не знает, что именно нужно сделать с числом, и делегирует это другой функции.

Анонимные функции в аргументах

Иногда создавать отдельную функцию ради одной строчки — слишком громоздко. Тогда можно передать анонимную функцию прямо в вызове.

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

    plusOne := applyToEach(numbers, func(x int) int {
        return x + 1
    })

    fmt.Println("Прибавили 1:", plusOne)
}

Функции в стандартной библиотеке

В Go часто встречается приём передачи функций в стандартных пакетах. Например, в sort.Slice мы задаём правила сортировки через функцию сравнения:

import (
    "fmt"
    "sort"
)

func main() {
    people := []string{"Иван", "Мария", "Алексей", "Сергей"}

    // сортировка по длине имени
    sort.Slice(people, func(i, j int) bool {
        return len(people[i]) < len(people[j])
    })

    fmt.Println(people) // [Иван Сергей Мария Алексей]
}

Здесь видно, что функции высшего порядка — это не теория, а повседневный инструмент.

Функции как результат

Функция может возвращать не только значения, но и другие функции. Такой приём называется замыканием.

// makeMultiplier возвращает функцию-множитель
func makeMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := makeMultiplier(2)
    triple := makeMultiplier(3)

    fmt.Println(double(5)) // 10
    fmt.Println(triple(5)) // 15
}

makeMultiplier создаёт функцию, которая “помнит” коэффициент factor.

Функции-обёртки (декораторы)

Функции высшего порядка можно использовать для оборачивания логики — например, добавить логирование:

import "fmt"

// decorator принимает функцию и возвращает новую с логированием
func decorator(f func(int) int) func(int) int {
    return func(x int) int {
        fmt.Println("Вызов с:", x)
        result := f(x)
        fmt.Println("Результат:", result)
        return result
    }
}

func square(x int) int {
    return x * x
}

func main() {
    loggedSquare := decorator(square)
    loggedSquare(5)
}

Именованные типы для функций

Чтобы код выглядел аккуратнее, можно объявлять собственный тип для функций:

type Transformer func(int) int

func apply(nums []int, f Transformer) []int {
    result := make([]int, len(nums))
    for i, v := range nums {
        result[i] = f(v)
    }
    return result
}

Это особенно удобно, если функция принимает сразу несколько функций как аргументы.

Ошибки и функции высшего порядка

Иногда функции возвращают ошибки. В таком случае обёртка тоже должна их обрабатывать:

type Worker func(string) (string, error)

func safeRun(f Worker) Worker {
    return func(s string) (string, error) {
        result, err := f(s)
        if err != nil {
            return "", fmt.Errorf("ошибка: %w", err)
        }
        return result, nil
    }
}

Параллель с map, filter и reduce

В Go нет встроенных функций map, filter и reduce, как в функциональных языках, но мы можем написать их сами:

func Map(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, v := range nums {
        result[i] = f(v)
    }
    return result
}

func Filter(nums []int, f func(int) bool) []int {
    result := []int{}
    for _, v := range nums {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce(nums []int, f func(int, int) int, init int) int {
    result := init
    for _, v := range nums {
        result = f(result, v)
    }
    return result
}

Итоги

Функции высшего порядка в Go позволяют писать код чище и понятнее. Они помогают вынести общий алгоритм в одну функцию и подставлять разные детали в виде других функций. При этом не обязательно создавать отдельные именованные функции — нужную логику можно написать прямо на месте с помощью анонимных. Такой приём даёт возможность реализовать преобразование коллекций наподобие map, filter и reduce.

Функции можно использовать и для обёрток: например, добавить к существующей операции логирование или обработку ошибок. Чтобы код оставался читаемым, удобно объявлять собственные типы для функций и работать с ними так же, как с любыми другими данными.

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


Самостоятельная работа

Используя функцию-декоратор расширим поведение других функций без их изменения. Например мы можем добавить логирование или профилирование.

Реализуйте функцию TimeDecorator, которая принимает функцию func(int) int и возвращает новую функцию с такой же сигнатурой. Новая функция измеряет время выполнения исходной функции, выводит его в консоль, а затем возвращает результат.

Пример использования:

func SlowDouble(x int) int {
    sum := 0
    for i := 0; i < 1_000_000; i++ {
        sum += x
    }
    return sum
}

func main() {
    timedDouble := timeDecorator(SlowDouble)
    fmt.Println(timedDouble(5)) // В консоль: "Execution time: ...", затем печать результата
}

Подсказки:

  • Используйте пакет time.
  • Внутри возвращаемой функции зафиксируйте start := time.Now(), затем вызовите исходную функцию, потом измерьте time.Since(start)
Показать решение
package main

import (
    "fmt"
    "time"
)

func TimeDecorator(f func(int) int) func(int) int {
    return func(x int) int {
        start := time.Now()
        result := f(x)
        duration := time.Since(start)
        fmt.Println("Execution time:", duration)
        return result
    }
}

func main() {
    slowDouble := func(x int) int {
        sum := 0
        for i := 0; i < 1_000_000; i++ {
            sum += x
        }
        return sum
    }

    timedDouble := TimeDecorator(slowDouble)

    fmt.Println(timedDouble(5))
}

Дополнительные материалы

  1. Go by Example — Collection Functions

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff