Замыкания
Перевод статьи "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, но для понимания основ это не так важно.