Замыкания
В прошлом уроке функция была карточкой с рецептом. Теперь прикрепим к карточке записку — переменную, с которой рецепт работает.
Функциональный литерал можно записать внутри main и сохранить в переменную. Такой литерал видит переменные main, объявленные выше него, и может их менять.
Литерал видит переменные
Литерал add прибавляет числа к переменной total, которая объявлена в main:
package main
import "fmt"
func main() {
total := 0
add := func(x int) { total += x }
add(5)
add(3)
fmt.Println(total) // 8
}Давайте разберём пример по строкам:
total := 0объявляет переменную вmain.add := func(x int) { total += x }сохраняет литерал в переменнуюaddтипаfunc(int). Литерал записан внутриmain, поэтому видит её переменные так же, как их видят строки самойmain.add(5)вызывается как обычная функция и прибавляет5кtotalизmain.add(3)прибавляет ещё3, и вtotalстановится8.fmt.Println(total)печатает8.
Переменная, а не копия
Параметр функции — это копия аргумента, но литерал, который использует total, копии не получает. Он работает с самой переменной total, поэтому меняет ту же переменную, которую потом печатает main.
Функцию, которая использует переменные из окружающего кода, называют замыканием. С «коротким замыканием» операторов && и || это слово не связано.
Здесь картинка с запиской нуждается в оговорке. Бумажная записка — копия: если потом исправить оригинал, записка останется прежней. Замыкание держит не копию, а саму переменную, поэтому связь работает в обе стороны. Если изменить переменную после создания литерала, вызов увидит новое значение:
x := 1
show := func() { fmt.Println(x) }
x = 2
show() // 2Разберём по строкам:
x := 1объявляет переменную со значением1.show := func() { fmt.Println(x) }создаёт литерал, когда вxлежит1.x = 2меняет переменную уже после создания литерала.show()читаетxв момент вызова и печатает2.
Несколько инструкций в литерале
В теле литерала может быть несколько инструкций, а в одной строке их разделяют точкой с запятой: func() int { n++; return n }. Такой литерал выполняет их по порядку:
n++увеличиваетn.return nвозвращает уже увеличенное значение.
Замыкание из функции
Функция может вернуть замыкание. Переменная, которую оно использует, продолжает жить после возврата из функции, пока возвращённую функцию хранит какая-нибудь переменная, например a ниже:
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
}Давайте разберём, что здесь происходит:
func counter() func() intобъявляет функцию, которая возвращает функцию типаfunc() int.n := 0создаёт новую переменнуюnпри каждом вызовеcounter.return func() int { … }возвращает новый литерал, который увеличивает этуnи возвращает её.a, b := counter(), counter()вызываетcounterдважды. Уaиbразные счётчики — две карточки, и у каждой своя записка.fmt.Println(a(), a(), b())выполняет вызовы в списке аргументов слева направо. Два вызоваaдают1и2, а первый вызовbснова даёт1.
Нажмите «Следующий шаг» и следите за строкой «захвачена»: у a и b своя n, и она живёт после возврата из counter.
1–4package main · import "fmt"5func counter() func() int {6 n := 07 return func() int {8 n++9 return n10 }11}12 13func main() {14 a, b := counter(), counter()15 fmt.Println(a(), a(), b())16}
main вызывает counter дважды: первый результат попадёт в a, второй — в b.
Снаружи изменить n нельзя: доступ к ней есть только у литерала.
Замыкания в цикле
Этот вопрос часто задают на собеседовании: что вернут замыкания, созданные в цикле for i := 0; i < 3; i++, если каждое возвращает i? Ответ зависит от версии Go:
- начиная с Go 1.22, у каждой итерации своя переменная
i, поэтому замыкания вернут0,1и2; - в более старых версиях все итерации делили одну переменную, и после цикла каждое замыкание видело
3.
Частая ошибка
Литерал пытается вызвать сам себя по имени переменной, в которую его записывают:
fact := func(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1) // ✗
}undefined: factИмя fact появляется только после всей инструкции :=, а внутри литерала его ещё нет. Исправление в два шага:
- Объявите переменную заранее:
var fact func(int) int. - Присвойте литерал отдельно:
fact = func(n int) int { … }.
Тогда литерал использует уже объявленную переменную fact.
Главное
- Замыкание — функция, которая использует переменные из окружающего кода.
- Замыкание работает с самой переменной, а не с копией, поэтому изменения видны в обе стороны.
- Каждый вызов
counter()создаёт новую переменнуюn, и у каждого счётчика своё состояние. - С Go 1.22 у каждой итерации цикла
forсвоя переменная, и замыкания из цикла видят разные значения.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.