Замыкания
Что такое замыкание
Замыкание (closure) — функция, которая «захватывает» переменные из окружающей области видимости. Эти переменные остаются доступны, даже когда внешняя функция уже завершилась.
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3Переменная count «захвачена» возвращённой функцией. Каждый вызов counter() увеличивает ту же count.
Несколько замыканий — одна переменная
Если создать два замыкания из одной фабрики — у каждого своя count:
c1 := makeCounter()
c2 := makeCounter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c2()) // 1 — независимый счётчикНо если два замыкания захватывают одну и ту же переменную — они её делят:
x := 0
inc := func() { x++ }
get := func() int { return x }
inc()
inc()
fmt.Println(get()) // 2Фабрики функций
Замыкания позволяют создавать функции, настроенные под конкретные параметры:
func makeMultiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15Паттерн «фабрика функций» очень распространён в middleware HTTP-серверов.
Мемоизация
Замыкания удобны для кэширования результатов:
func memoFib() func(int) int {
cache := map[int]int{}
var fib func(int) int
fib = func(n int) int {
if n <= 1 {
return n
}
if v, ok := cache[n]; ok {
return v
}
result := fib(n-1) + fib(n-2)
cache[n] = result
return result
}
return fib
}
fib := memoFib()
fmt.Println(fib(40)) // быстро — результаты кэшируютсяЛовушка в цикле
Классическая ошибка с горутинами и замыканиями:
// НЕПРАВИЛЬНО — все горутины захватят i = 3
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // вероятно: 3 3 3
}()
}
// ПРАВИЛЬНО — копия i создаётся для каждой горутины
for i := 0; i < 3; i++ {
i := i // теневая переменная
go func() {
fmt.Println(i) // 0 1 2 (в произвольном порядке)
}()
}Замыкание захватывает переменную, а не значение. К моменту выполнения горутины цикл может уже завершиться.
В Go 1.22+ поведение
for-цикла изменилось: каждая итерация создаёт новую переменную, поэтому первый вариант тоже работает правильно.
Функциональные опции (паттерн)
Популярный паттерн в Go — передавать конфигурацию через функциональные опции:
type Server struct {
timeout int
maxConn int
}
type Option func(*Server)
func WithTimeout(t int) Option {
return func(s *Server) {
s.timeout = t
}
}
func NewServer(opts ...Option) *Server {
s := &Server{timeout: 30, maxConn: 100}
for _, opt := range opts {
opt(s)
}
return s
}
srv := NewServer(WithTimeout(60))Это и есть замыкания в реальном применении — каждый With* возвращает функцию, настраивающую структуру.
makeCounter() создаёт новое замыкание с собственной копией переменной count. Счётчики c1 и c2 полностью независимы.