← Назад к вопросам

Как работает замыкание?

1.6 Junior🔥 121 комментариев
#Основы Go

Комментарии (1)

🐱
gpt-5-nanoPrepBro AI4 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает замыкание в Go

Что такое замыкание и зачем оно нужно

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

  • Основной принцип: лексическое окружение сохраняется вместе с функцией.
  • В Go это может приводить к тому, что переменные попадают в heap (кучу) из-за так называемого escape-анализа, если замыкание живёт дольше текущей функции.

Как переменные захватываются

  • Захват происходит по ссылке: замыкание обращается к одной и той же переменной, которая существует в окружении.
  • Это позволяет накапливать состояние между вызовами замыкания, но требует аккуратности, чтобы не столкнуться с ловушками в циклах и в конкурентном коде.

Примеры

  • Простой счетчик-замыкание:
package main

import "fmt"

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c := makeCounter()
    fmt.Println(c()) // 1
    fmt.Println(c()) // 2
    fmt.Println(c()) // 3
}

Здесь переменная count захвачена замыканием и сохраняется между вызовами. Это поведение демонстрирует, что замыкание может «жить» дольше своей исходной функции.

  • Ловушки с циклами и диапазонами:
package main

import "fmt"

func makePrintFuncs(items []string) []func() {
    var funcs []func()
    for _, s := range items {
        // чтобы зафиксировать текущее значение s в каждой функции
        s := s
        funcs = append(funcs, func() {
            fmt.Println(s)
        })
    }
    return funcs
}

func main() {
    items := []string{"a", "b", "c"}
    fns := makePrintFuncs(items)
    for _, f := range fns {
        f() // выводит a, затем b, затем c
    }
}

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

Важные детали

  • Эскейп-аналитик Go решает, размещать захваченные переменные на стеке или на куче:
    • если замыкание не выходит за пределы функции и переменные не сохраняются вне её, переменные остаются на стеке.
    • если замыкание живёт дольше, чем функция, и переменная нужна извне, их перемещают на кучу.
  • Гораздо чаще встречаются ситуации с конкурентным доступом:
    • если несколько горутин обращаются к одному и тому же замыканию и его захваченным переменным, потребуется синхронизация (мьютексы, каналы) или копирование значений.

Лучшие практики

  • Используйте замыкания для сохранения локального состояния и создания фабрик функций, обработчиков событий и т.д.
  • Избегайте накопления больших структур в замыканиях, чтобы не перегружать кучу.
  • Будьте осторожны с циклами и диапазоном: фиксируйте текущее значение переменной внутри цикла, если нужно получить ожидаемое поведение.
  • При работе с конкурентным кодом обязательно синхронизацию вокруг общих захваченных переменных или используйте локальные копии внутри замыкания.

Коротко о концепциях

  • замыкание в Go — это функция, которая имеет доступ к своим внешним переменным.
  • Захвачиваемые переменные — это захваченные переменные (captured variables), которые могут жить дольше своей исходной области.
  • Вопрос об размещении в памяти решается escape-анализом: иногда переменные уходят в heap, иногда остаются на stack.
  • Замыкания позволяют писать выразительный и модульный код, но требуют понимания поведения захвата переменных и потенциальных условий гонки.