Замыкания

20 minute read

Перевод статьи "Closures in golang"

Замыкания - это такие функции, которые вы можете создавать в рантайме и им будет доступно текущее окружение, в рамках которого они были созданы. Другими словами, функции, определенные как замыкания, "запоминают" окружение, в котором они были созданы.

Перед тем как мы начнем разбираться с замыканиями, давайте определимся с функциями и анонимными функциями.

Анонимные функции

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

Функция getPrintMessage создает анонимную функцию и возвращает ее. В printfunc сохраняется анонимная функция, которая затем вызывается.

package main

import "fmt"

func printMessage(message string) {
    fmt.Println(message)
}

func getPrintMessage() func(string) {
    // Возвращаем анонимную функцию
    return func(message string) {
        fmt.Println(message)
    }
}

func main() {
    // Именованная функция
    printMessage("Hello function!")

    // Анонимная фукция объявляется и вызывается
    func(message string) {
        fmt.Println(message)
    }("Hello anonymous function!")

    // Получаем анонимную функцию и вызываем ее
    printfunc := getPrintMessage()
    printfunc("Hello anonymous function using caller!")

}

Замыкания

Ниже, функция foo это внутренняя функция и у нее есть доступ к переменной text, определенной за рамками функции foo но внутри функции outer. Вот эта функция foo и называется замыканием. Она как бы замыкает переменные из внешней области видимости. Внутри нашего замыкания переменная text будет доступна.

package main

import "fmt"

func outer(name string) {
    // переменная из внешней функции
    text := "Modified " + name

    // foo это внутренняя функция и из нее есть доступ к переменной `text`
    // у замыкания есть доступ к этим переменным даже после выхода из блока 
    foo := func() {
        fmt.Println(text)
    }

    // вызываем замыкание
    foo()
}

func main() {
    outer("hello")
}

Возвращаем замыкание и используем его снаружи

В этом примере покажем как можно возвращать замыкание из функции, в которой оно было определено. foo это замыкание, которое возвращается в главную функцию когда внешняя функции вызывается. А вызов самого замыкания происходит в момент, когда используются (). Этот код выводит сообщение "Modified hello". Таким образом, в замыкании foo все еще доступна переменная text, хотя мы уже вышли из внешней функции.

package main

import "fmt"

func outer(name string) func() {
    // переменная
    text := "Modified " + name

    // замыкание. у функции есть доступ к переменной text даже 
    // после выхода за пределы блока
    foo := func() {
        fmt.Println(text)
    }

    // возвращаем замыкание
    return foo
}

func main() {
    // foo это наше замыкание
    foo := outer("hello")

    // вызов замыкания
    foo()
}

Замыкание и состояние

Замыкания сохраняют состояние. Это означает, что состояние переменных содержится в замыкании в момент декларации. Что это значит:

  • Состояние(ссылки на переменные) такие же как и в момент создания замыкания. Все замыкания созданные вместе имеют общее состояние.
  • Состояния будут разными если замыкания создавались по разному.

Давайте посмотрим на код ниже. Мы реализуем функцию, которая принимает начальное значение и возвращает два замыкания: counter(str) и incrementer(incr). И в этом случае, состояние(переменная start) будет одинаковым для обоих замыканий. После следующего вызова функции counter, мы получим еще два замыкания с уже новым состоянием.

В нашем примере при первом вызове counter(100) мы получаем замыкания ctr, intr в которых сохранен один и тот же указатель на 100.

package main

import "fmt"

func counter(start int) (func() int, func()) {
    // если значение мутирует, то мы получим изменение и в этом замыкании
    ctr := func() int {
        return start
    }

    incr := func() {
        start++
    }

    // и в ctr, и в incr сохраняется указатель на start
    // мы создали замыкания но еще не вызывали
    return ctr, incr
}

func main() {
    // ctr, incr и ctr1, incr1 различаются своим состоянием
    ctr, incr := counter(100)
    ctr1, incr1 := counter(100)
    fmt.Println("counter - ", ctr())
    fmt.Println("counter1 - ", ctr1())
    // увеличиваем на 1
    incr()
    fmt.Println("counter - ", ctr())
    fmt.Println("counter1- ", ctr1())
    // увеличиваем до 2
    incr1()
    incr1()
    fmt.Println("counter - ", ctr())
    fmt.Println("counter1- ", ctr1())
}

Как видите, изначально оба значение равны 100. И когда мы увеличиваем значение с помощью incr(), замыкание ctr1() выводит старое значение, а ctr() выводит уже 101. Точно так же, если вызывать замыкание incr1(), то ctr() будет всегда выводить 101, а ctr1() будет показывать новые значения.

ctr1() would be 102.
counter -  100
counter1 -  100
counter -  101
counter1-  100
counter -  101
counter1-  102

Ловушки

Одна из самых очевидных ловушек - это создание замыканий в цикле. Рассмотрим пример кода ниже.

Мы создаем 4 замыкания в цикле и возвращаем слайс с замыканиями. Каждое замыкание выполняет одинаковые действия: выводит индекс и значение по этому индексу. Главная функция проходит по слайсу и вызывает все эти замыкания.

package main

import "fmt"

func functions() []func() {
    // ловушка с циклами
    arr := []int{1, 2, 3, 4}
    result := make([]func(), 0)

    // функции не вызываются, только определяются и возвращаются
    // так как функции используют i и arr[i]
    // то они будут работать только с последними значениями i
    for i := range arr {
        result = append(result, func() { fmt.Printf("index - %d, value - %d\n", i, arr[i]) })
    }

    // если такое поведение необходимо, то следует использовать параметры
    //for i := range arr {
    //  result = append(result, func(index, val int) { fmt.Printf("index - %d, value - %d\n", index, val) })
    //}

    return result
}

func main() {
    fns := functions()
    for f := range fns {
        fns[f]()
    }
}

Посмотрим на результат выполнения скрипта

index - 3, value - 4
index - 3, value - 4
index - 3, value - 4
index - 3, value - 4

Не очень приятный сюрприз. Давайте разберемся почему. Если мы вернемся на пару шагов назад и вспомним, что замыкания создаются единожды и имеют общее состояние, то проблема начинает проясняться. В нашем случае, все замыкания ссылаются на одни и теже переменные i и arr. Когда замыкания вызываются в главной функции, значение i равно 3 и значение во всех замыканиях мы получаем по ключу arr[3].

Что в результате

Надеюсь, что поле прочтения этой статьи, вы чуть лучше понимаете принципы работы с замыканиями и теперь вам проще читать код в котором используются замыкания.

Для более углубленного понимания рекомендую прочитать две замечательные статьи, указанные ниже. В этих статьях говорится о замыканиях в контексте JavaScript, но для понимания основ это не так важно.