ПрограммированиеSenior Go разработчик

Расскажите, как Go реализует замыкания (func literals/closures) и каковы ограничения и особенности использования замыканий: где они хранятся, как захватываются переменные, чем отличается поведение захваченных переменных при разных сценариях?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В Go анонимные функции (func literals) способны создавать замыкания, то есть получать доступ к переменным из окружающей области видимости даже после её завершения. Такие замыкания выделяют память в heap, если это нужно для корректной работы (detected via escape analysis).

Пример:

func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d ", a(5)) // 5 printf("%d ", a(10)) // 15

Особенности:

  • Замыкание захватывает по ссылке переменные внешней области (а не их значения на момент создания).
  • Если переменная изменяется вне замыкания — замыкание увидит новое значение.
  • Если замыкание возвращается из функции, захваченные переменные будут жить до конца жизни closure.
  • Если замыкание не используется, escape analysis может позволить переменным не уходить в кучу.

Вопрос с подвохом

Что выведет этот код?

func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }

Многие ответят, что выведет 0, 1, 2, однако результат будет:

3
3
3

Все замыкания ссылаются на одну и ту же переменную i; после завершения цикла её значение — 3.

Правильно: захватывать копию переменной в теле цикла:

for i := 0; i < 3; i++ { v := i // новая переменная fs = append(fs, func() { fmt.Println(v) }) }

Примеры реальных ошибок из-за незнания тонкостей темы


История

В проекте динамического роутинга использовали цикл для создания множества handler-ов через closure, каждый должен был захватить свой путь. В результате все handler-ы печатали последний путь — не создали отдельную переменную в каждом замыкании. Ошибка обнаружилась только при интеграции с HTTP API.


История

При тестировании параллельного доступа через горутины внутри цикла closure захватывало ссылку на индекс, а не копию. Это создавало "случайные" эффекты: данные записывались не в свой слот массива, а в последний.


История

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