Что такое замыкание (closure) в Go и в чём его особенность при работе с циклами?

Ответ

Замыкание (closure) — это функция, которая «захватывает» переменные из внешней области видимости (лексического окружения), в которой она была создана. Эта функция может обращаться к этим переменным, даже когда выполнение внешней функции уже завершилось.

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

Классический пример (счётчик):

func newCounter() func() int {
    // 'sum' - это захваченная переменная
    sum := 0
    return func() int {
        sum++
        return sum
    }
}

func main() {
    counter := newCounter()
    fmt.Println(counter()) // Вывод: 1
    fmt.Println(counter()) // Вывод: 2
    // Каждый вызов работает с одной и той же переменной 'sum'
}

В этом примере анонимная функция, возвращаемая из newCounter, является замыканием. Она «помнит» свою собственную переменную sum, которая сохраняет свое состояние между вызовами.

Частая ловушка: замыкания в циклах

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

Неправильный код:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // Все горутины захватывают ОДНУ И ТУ ЖЕ переменную 'i'.
            // К моменту их выполнения цикл уже завершится, и 'i' будет равно 3.
            fmt.Println(i)
        }()
    }
    wg.Wait()
    // Вероятный вывод (порядок не гарантирован):
    // 3
    // 3
    // 3
}

Как это исправить?

Нужно передать значение переменной цикла как аргумент в анонимную функцию. Это создаст копию переменной для каждой итерации.

Правильный код:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        // Передаем 'i' как аргумент, создавая локальную копию 'val'.
        go func(val int) {
            defer wg.Done()
            fmt.Println(val)
        }(i)
    }
    wg.Wait()
    // Вывод (порядок не гарантирован):
    // 0
    // 1
    // 2
}

Ответ 18+ 🔞

А, замыкания, блядь! Ну это ж классика, ёпта! Сейчас объясню, как эти штуки работают, а то народ постоянно в одну и ту же ловушку попадает, как последние распиздяи.

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

Вот смотри, классический пример — счётчик, который помнит своё состояние:

func newCounter() func() int {
    // 'sum' - это захваченная переменная
    sum := 0
    return func() int {
        sum++
        return sum
    }
}

func main() {
    counter := newCounter()
    fmt.Println(counter()) // Вывод: 1
    fmt.Println(counter()) // Вывод: 2
    // Каждый вызов работает с одной и той же переменной 'sum'
}

Видишь? Функция newCounter родила анонимную функцию, и та прихватила с собой переменную sum. Теперь эта переменная живёт внутри замыкания, и каждый раз, когда мы дергаем counter(), sum увеличивается. Красота, да? Но это цветочки.

А теперь ягодки, блядь! Самая распространённая ебля, в которую все попадают — это замыкания в циклах. Народ как дурак делает:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // Все горутины захватывают ОДНУ И ТУ ЖЕ переменную 'i'.
            // К моменту их выполнения цикл уже завершится, и 'i' будет равно 3.
            fmt.Println(i)
        }()
    }
    wg.Wait()
    // Вероятный вывод (порядок не гарантирован):
    // 3
    // 3
    // 3
}

И потом охуевают: "Почему у меня все три горутины выводят тройку, а не 0, 1, 2?!" Да потому что ты, гений, все три анонимные функции заставил хватать одну и ту же переменную i из внешней области! Они все смотрят на одну ячейку памяти, блядь! А к моменту, когда горутины-лентяи наконец-то запустятся, цикл уже давно прокрутится до конца, и в этой ячейке будет лежать тройка. Вот они все и напечатают 3. Пиздец логика.

Как делать правильно, ёпта?

Надо передавать значение как аргумент! Тогда для каждой итерации цикла создастся своя локальная копия, и каждая горутина получит своё собственное значение, а не будет все дружно смотреть в одну дырку.

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        // Передаем 'i' как аргумент, создавая локальную копию 'val'.
        go func(val int) {
            defer wg.Done()
            fmt.Println(val)
        }(i)
    }
    wg.Wait()
    // Вывод (порядок не гарантирован):
    // 0
    // 1
    // 2
}

Вот видишь разницу? Мы передаём i в функцию как аргумент val. В этот момент Go создаёт копию текущего значения i для этой конкретной итерации. Каждая горутина получает свою собственную копию числа и спокойно с ней работает. Больше никаких сюрпризов.

Запомни это, а то будешь как все — часами дебажить, почему твои горутины ведут себя не так, а потом окажется, что ты просто забыл про эту ебаную особенность замыканий. В рот меня чих-пых, сколько нервов на этом потрачено!