Перейти к содержимому
Теория · Функции как значения · урок 3 из 12

Замыкания

В прошлом уроке функция была карточкой с рецептом. Теперь прикрепим к карточке записку — переменную, с которой рецепт работает.

Функциональный литерал можно записать внутри main и сохранить в переменную. Такой литерал видит переменные main, объявленные выше него, и может их менять.

Литерал видит переменные

Литерал add прибавляет числа к переменной total, которая объявлена в main:

GO
package main

import "fmt"

func main() {
    total := 0
    add := func(x int) { total += x }
    add(5)
    add(3)
    fmt.Println(total) // 8
}

Давайте разберём пример по строкам:

  1. total := 0 объявляет переменную в main.
  2. add := func(x int) { total += x } сохраняет литерал в переменную add типа func(int). Литерал записан внутри main, поэтому видит её переменные так же, как их видят строки самой main.
  3. add(5) вызывается как обычная функция и прибавляет 5 к total из main.
  4. add(3) прибавляет ещё 3, и в total становится 8.
  5. fmt.Println(total) печатает 8.

Переменная, а не копия

Параметр функции — это копия аргумента, но литерал, который использует total, копии не получает. Он работает с самой переменной total, поэтому меняет ту же переменную, которую потом печатает main.

Функцию, которая использует переменные из окружающего кода, называют замыканием. С «коротким замыканием» операторов && и || это слово не связано.

Здесь картинка с запиской нуждается в оговорке. Бумажная записка — копия: если потом исправить оригинал, записка останется прежней. Замыкание держит не копию, а саму переменную, поэтому связь работает в обе стороны. Если изменить переменную после создания литерала, вызов увидит новое значение:

GO
x := 1
show := func() { fmt.Println(x) }
x = 2
show() // 2

Разберём по строкам:

  1. x := 1 объявляет переменную со значением 1.
  2. show := func() { fmt.Println(x) } создаёт литерал, когда в x лежит 1.
  3. x = 2 меняет переменную уже после создания литерала.
  4. show() читает x в момент вызова и печатает 2.

Несколько инструкций в литерале

В теле литерала может быть несколько инструкций, а в одной строке их разделяют точкой с запятой: func() int { n++; return n }. Такой литерал выполняет их по порядку:

  1. n++ увеличивает n.
  2. return n возвращает уже увеличенное значение.

Замыкание из функции

Функция может вернуть замыкание. Переменная, которую оно использует, продолжает жить после возврата из функции, пока возвращённую функцию хранит какая-нибудь переменная, например a ниже:

GO
func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    a, b := counter(), counter()
    fmt.Println(a(), a(), b()) // 1 2 1
}

Давайте разберём, что здесь происходит:

  1. func counter() func() int объявляет функцию, которая возвращает функцию типа func() int.
  2. n := 0 создаёт новую переменную n при каждом вызове counter.
  3. return func() int { … } возвращает новый литерал, который увеличивает эту n и возвращает её.
  4. a, b := counter(), counter() вызывает counter дважды. У a и b разные счётчики — две карточки, и у каждой своя записка.
  5. fmt.Println(a(), a(), b()) выполняет вызовы в списке аргументов слева направо. Два вызова a дают 1 и 2, а первый вызов b снова даёт 1.

Нажмите «Следующий шаг» и следите за строкой «захвачена»: у a и b своя n, и она живёт после возврата из counter.

ВживуюДва счётчикашаг 1 из 10 · рамок: 1

main вызывает counter дважды: первый результат попадёт в a, второй — в b.

Строка «захвачена» — переменные, которые держат замыкания

Снаружи изменить n нельзя: доступ к ней есть только у литерала.

Замыкания в цикле

Этот вопрос часто задают на собеседовании: что вернут замыкания, созданные в цикле for i := 0; i < 3; i++, если каждое возвращает i? Ответ зависит от версии Go:

  • начиная с Go 1.22, у каждой итерации своя переменная i, поэтому замыкания вернут 0, 1 и 2;
  • в более старых версиях все итерации делили одну переменную, и после цикла каждое замыкание видело 3.

Частая ошибка

Литерал пытается вызвать сам себя по имени переменной, в которую его записывают:

GO
fact := func(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1) // ✗
}
TEXT
undefined: fact

Имя fact появляется только после всей инструкции :=, а внутри литерала его ещё нет. Исправление в два шага:

  1. Объявите переменную заранее: var fact func(int) int.
  2. Присвойте литерал отдельно: fact = func(n int) int { … }.

Тогда литерал использует уже объявленную переменную fact.

Главное

  • Замыкание — функция, которая использует переменные из окружающего кода.
  • Замыкание работает с самой переменной, а не с копией, поэтому изменения видны в обе стороны.
  • Каждый вызов counter() создаёт новую переменную n, и у каждого счётчика своё состояние.
  • С Go 1.22 у каждой итерации цикла for своя переменная, и замыкания из цикла видят разные значения.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом