Оригинальная ссылка:Детали закрытия языка 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 передаются по значению.
Таким образом, это эквивалентно объявлению трех переменных в этой новой анонимной функции, на которые независимо ссылаются три функции замыкания. Принцип тот же, что и в первом способе.
Приведенное здесь решение можно использовать для большинства проблем, связанных со ссылками на замыкание, не ограничиваясь третьим примером.