Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

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

Recommended resources

Здравствуйте!

На прошлом уроке мы освоили одну из ключевых идиом Go — возврат нескольких значений из функций, особенно паттерн (value, error). Это заложило основу для понимания явного потока управления в языке. Сегодня мы продолжим исследовать возможности функций и рассмотрим концепцию, которая будет вам очень знакома благодаря вашему опыту в JavaScript: функции как объекты первого класса.

Цель этого урока — научиться передавать функции как значения и создавать замыкания (closures) с контролируемым захватом состояния. В Go, как и в JavaScript, функции можно присваивать переменным, передавать в качестве аргументов и возвращать из других функций. Это открывает дорогу к мощным паттернам, таким как инкапсуляция состояния и создание гибких, конфигурируемых компонентов. Мы также разберем классическую "ловушку", связанную с захватом переменных в циклах, — проблему, с которой вы, вероятно, уже сталкивались.

Функции как объекты первого класса

Концепция "first-class citizens" (объектов первого класса) означает, что сущность (в данном случае, функция) может рассматриваться как любое другое значение в языке. Ее можно хранить в переменных, передавать в другие функции и возвращать из них.

Mastering Functions in Go: A Beginner's Guide to ...

Для начала, давайте познакомимся с этой идеей. Прочтите короткий раздел, который вводит понятие "first-class citizens" в Go.

Найдите подзаголовок "Advanced Topics". Сосредоточьтесь на абзаце, который начинается со слов "Functions are first-class-citizens".

Это свойство позволяет писать более выразительный и гибкий код. Давайте посмотрим, как это работает на практике.

1. Присваивание функции переменной

Функцию, в том числе анонимную (без имени), можно присвоить переменной. Тип такой переменной — это сама сигнатура функции.

// add - это переменная типа func(a, b int) int
add := func(a, b int) int {
    return a + b
}

result := add(10, 20) // result == 30

Здесь мы объявили анонимную функцию и присвоили ее переменной add. Теперь add можно вызывать как обычную функцию.

2. Передача функции как аргумента

Одна из самых частых областей применения функций как значений — передача их в качестве аргументов в другие функции. Это позволяет "внедрять" поведение. Например, стандартная функция sort.Slice принимает функцию сравнения, которая определяет, как сортировать срез.

Давайте рассмотрим более простой пример.

Этот код демонстрирует функцию `calc`, которая принимает два целых числа и третьим аргументом — функцию `f`. В зависимости от того, какая функция передана (`add` или `subtract`), `calc` выполняет соответствующую операцию.

Этот паттерн очень мощный. Он позволяет отделить общую логику (в данном случае, вызов некой операции над двумя числами) от конкретной реализации этой операции.

Mastering Functions in Go: A Beginner's Guide to ...

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

Найдите раздел "Functions as Parameters" и прочитайте его до конца. Особое внимание обратите на разбор сигнатуры функции calculate. Автор также отлично объясняет разницу между передачей самой функции (add) и передачей результата ее вызова (add(10, 2)), что является частой ошибкой новичков.

3. Возврат функции из функции

Наконец, функция может возвращать другую функцию. Это является основой для создания замыканий.

Mastering Functions in Go: A Beginner's Guide to ...

Прочтите раздел о возврате функций. Это подготовит нас к следующей теме — замыканиям.

Найдите раздел "Functions as Return Values". Изучите пример с функцией timer, которая возвращает другую функцию для замера времени.


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

Когда функция возвращает другую (вложенную) функцию, эта вложенная функция "запоминает" свое лексическое окружение — переменные, которые были доступны в момент ее создания. Такая пара из функции и ее окружения называется замыканием.

Это позволяет создавать функции с состоянием.

How to Use Closures in Go

Давайте разберемся, как замыкания работают в Go, на классическом примере счетчика.

Прочтите статью, начиная с раздела "What Closures Really Are" и до конца раздела "Multiple Independent Closures". Обратите внимание на два ключевых момента: Переменная n "переживает" вызов внешней функции counter(). Каждый вызов counter() создает независимый экземпляр замыкания со своим собственным состоянием n. В статье также упоминается escape analysis — это механизм, с помощью которого компилятор Go определяет, что переменную нужно переместить из стека в кучу, чтобы она оставалась доступной для замыкания. Это важное замечание для понимания "под капотом".


Классическая ловушка с переменной цикла

Существует одна очень распространенная ошибка, связанная с замыканиями, в которую попадают даже опытные разработчики. Она возникает при создании замыканий внутри цикла. Эта проблема и ее решение в Go практически идентичны тому, как это работало в JavaScript с ключевым словом var.

How to Use Closures in Go

Теперь разберем эту важную ловушку и идиоматичный способ ее избежать.

Прочтите раздел "The Classic Loop Trap". Постарайтесь понять, почему все созданные функции выводят одно и то же значение. Затем внимательно изучите предложенное решение — создание новой переменной внутри цикла (i := i). Это ключевой идиоматический паттерн в Go.

Суть проблемы в том, что все анонимные функции, созданные в цикле, захватывают одну и ту же переменную i по ссылке. К моменту вызова этих функций цикл уже завершился, и i имеет свое конечное значение.

Решение, i := i, создает на каждой итерации новую, "затеняющую" переменную i, которая является локальной для тела цикла. Именно эту новую переменную и захватывает замыкание, получая корректное значение для данной итерации.


Итоги

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

  • Функции — объекты первого класса: их можно присваивать переменным, передавать в другие функции и возвращать из них.
  • Тип функции определяется ее сигнатурой, например, func(string, int) bool.
  • Замыкание (closure) — это функция, которая "захватывает" переменные из своего внешнего лексического окружения, сохраняя состояние между вызовами.
  • Каждый вызов функции, возвращающей замыкание, создает независимый экземпляр состояния.
  • При создании замыканий в цикле необходимо явно захватывать значение итерационной переменной для каждой итерации, используя идиому i := i, чтобы избежать "ловушки с переменной цикла".

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

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