Оригинальная ссылка:Детали закрытия языка Go
Что такое закрытие?Замыкание — это сущность, состоящая из функции и связанной с ней эталонной среды.
Давайте возьмем несколько примеров, чтобы проиллюстрировать замыкания в Go и проблемы, вызванные ссылками на замыкания.
функциональная переменная (значение функции)
Прежде чем объяснять закрытие, давайте сначала разберемся, что такоефункциональная переменная.
В Go функции рассматриваются какпервоклассная ценность, что означает, что у функций, как и у переменных, есть типы и значения, и они могут делать то же, что и другие обычные переменные.
func square(x int) {
println(x * x)
}
- Звоните напрямую:
square(1)
- Назначайте функции, как если бы они были переменными:
s := square
; затем можно вызвать функциональную переменную:s(1)
.Примечание: здесьsquare
Скобки после вызова не ставятся.
- перечислить
nil
Функциональная переменная будет паниковать. - Нулевое значение функциональной переменной равно
nil
, что означает, что за ним может следоватьnil
сравнение, но не между двумя функциональными переменными.
Закрытие
Теперь давайте проиллюстрируем замыкания примерами:
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
Вызов этой функции возвращает функциональную переменную.
i := incr()
: Назначив эту функциональную переменнуюi
,i
сталЗакрытие.
такi
сохранить правоx
цитаты, мыслимыеу меня есть указатель на хилиу меня есть адрес x в нем.
так какi
иметь точку зренияx
указатель, поэтому его можно изменитьx
, и сохранить состояние:
println(i()) // 1
println(i()) // 2
println(i()) // 3
Это,x
Сбежав, его жизнь не заканчивается с окончанием его области действия.
Но этот код не увеличивается:
println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
Это потому, что он вызывается здесь три разаincr()
, который возвращает три замыкания, ссылающиеся на три разныхx
, их состояния независимы друг от друга.
ссылка на закрытие
Теперь давайте начнем с примера, чтобы проиллюстрировать проблемы, вызванные замыкающими ссылками:
x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3
Поскольку замыкание является внешней лексической переменной области видимости.Цитировать, поэтому этот код выведет3.
мыслимыйf
хранится вx
адрес, он используетx
будет разыменован напрямую, поэтомуx
Изменение значения приведет кf
Разыменованное значение также изменяется.
Однако этот код выведет1:
x := 1
func() {
println(x) // 1
}()
x = 2
x = 3
Это легче понять, преобразовав его во что-то вроде этого:
x := 1
f := func() {
println(x)
}
f() // 1
x = 2
x = 3
Это потому чтоf
Значение было разыменовано при вызове, и последующие модификации не имеют к этому никакого отношения.
Но если вы снова позвонитеf
все равно будет выводить3, что еще раз доказываетf
хранится вx
адрес г.
Это можно доказать, напечатав адрес переменной, на которую ссылаются, внутри и снаружи замыкания:
x := 1
func() {
println(&x) // 0xc0000de790
}()
println(&x) // 0xc0000de790
Вы можете видеть, что ссылка на тот же адрес.
Справочник по круговым замыканиям
Следующие три примера иллюстрируют проблемы, создаваемые замыкающими ссылками внутри циклов:
первый пример
for i := 0; i < 3; i++ {
func() {
println(i) // 0, 1, 2
}()
}
Этот код эквивалентен:
for i := 0; i < 3; i++ {
f := func() {
println(i) // 0, 1, 2
}
f()
}
Сразу после каждой итерацииi
Выполните разыменование и используйте полученное значение и не используйте его снова, чтобы этот код выдавался нормально.
второй пример
Обычный код: вывод0, 1, 2:
var dummy [3]int
for i := 0; i < len(dummy); i++ {
println(i) // 0, 1, 2
}
Однако этот код выведет3:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
f = func() {
println(i)
}
}
f() // 3
Ранее я упоминал, что замыкание принимает ссылку, поэтому этот код должен выводитья последнее значение 2Правильно?
неправильный. Это потому чтоi
Окончательное значение не2.
Преобразование цикла в эту форму легче понять:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
f = func() {
println(i)
}
i++
}
f() // 3
i
добавлен3выскочит из петли, поэтому после окончания петлиi
Окончательное значение3.
так что используйтеfor range
Для реализации этого примера это не будет работать так:
var dummy [3]int
var f func()
for i := range dummy {
f = func() {
println(i)
}
}
f() // 2
Это потому чтоfor range
иfor
Базовая реализация отличается.
третий пример
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 3, 3, 3
}
Выходная последовательность3, 3, 3.
Посмотрев на предыдущий пример, здесь легко понять:
Все три функции ссылаются на одну и ту же переменную (i
), так послеi
Увеличение, значение, полученное путем разыменования, также будет увеличиваться, поэтому все три функции будут выводить3.
Код для добавления выходного адреса доказывает, что:
var funcSlice []func()
for i := 0; i < 3; i++ {
println(&i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
funcSlice = append(funcSlice, func() {
println(&i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
}
Вы можете видеть, что ссылки на три функцииi
адрес г.
Решение
1. Объявите новую переменную:
- Объявить новые переменные:
j := i
, и поставить справа послеi
Операция меняется наj
работать. - Объявите новую переменную с тем же именем:
i := i
.Примечание. Правая часть короткого оператора здесь — это внешняя область видимости.i
, левая сторона — это недавно объявленная область действия на этом уровне.i
. Принцип тот же, что и выше.
Это эквивалентно объявлению переменной для каждой из трех функций, всего 3. Начальные значения этих трех переменных соответствуют значениям в цикле.i
И после этого не изменится.
2. Объявить новую анонимную функцию и передать параметры:
var funcSlice []func()
for i := 0; i < 3; i++ {
func(i int) {
funcSlice = append(funcSlice, func() {
println(i)
})
}(i)
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 0, 1, 2
}
в настоящее времяprintln(i)
в использованииi
передается параметрами функции, а параметры функции в языке Go передаются по значению.
Таким образом, это эквивалентно объявлению трех переменных в этой новой анонимной функции, на которые независимо ссылаются три функции замыкания. Принцип тот же, что и в первом способе.
Приведенное здесь решение можно использовать для большинства проблем, связанных со ссылками на замыкание, не ограничиваясь третьим примером.