Методы приемника указателя и переменные итерации
Еще одна проблема с повторным использованием переменных в предложении диапазона.
Этот рассказ начнется с загадки (исходный код):
type T struct { name string } func (t T) M(wg *sync.WaitGroup) { fmt.Println(t.name) wg.Done() } func main() { var wg sync.WaitGroup for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { wg.Add(1) go v.M(&wg) } wg.Wait() }
Задумайтесь на минутку, что будет записано в stdout? Ответ не так прост. На самом деле это может быть много разных вещей. Программа всегда будет печатать foo
, bar
и baz
, каждая в отдельной строке, но порядок может быть каждый раз разным:
baz foo bar
Как запускать горутины действительно зависит от планировщика. Планировщик не является темой этого поста. Давайте немного изменим эту программу, чтобы вместо нее использовать приемник указателя (исходный код):
func (t *T) M(wg *sync.WaitGroup) { fmt.Println(t.name) wg.Done() }
Это также зависит от планировщика среды выполнения, но обычно я получаю одну и ту же строку трижды:
baz baz baz
Если вас заинтриговало, почему он отличается от первого примера, продолжайте читать. Этот пост объяснит, что здесь происходит на самом деле.
defer vs go заявления
Стоит отметить, что мы получим аналогичное поведение, если заменим оператор go на оператор defer (исходный код):
type T struct { name string } func (t T) M() { fmt.Println(t.name) } func main() { for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { defer v.M() } }
Выход:
baz bar foo
Это немного отличается от кода с оператором go, поскольку вывод является детерминированным - он не зависит от планировщика (отложенные функции вызываются в порядке LIFO). Если мы будем использовать приемник указателя, то мы также будем детерминированным поведением, но результат будет разным (исходный код):
type T struct { name string } func (t *T) M() { fmt.Println(t.name) } func main() { for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { defer v.M() } }
Выход:
baz baz baz
Это снова одна и та же строка трижды. Чтобы немного упростить наш анализ, давайте сначала сосредоточимся на case с помощью defer statement, поскольку он дает детерминированное поведение.
Многие люди уже знают, что циклы и замыкания могут быть сложными (исходный код):
for _, v := range []string{"foo", "bar", "baz"} { defer func() { fmt.Println(v) }() }
Выход:
baz baz baz
Это не является чем-то особенным для Голанга. То же самое происходит на других языках, таких как JavaScript (исходный код):
(function() { var funcs = []; for (var v of ["foo", "bar", "baz"]) { funcs.push(function() { console.info(v); }); } funcs.forEach(f => f()); })();
В консоли браузера вы три раза увидите baz
. С предложением range нам нужно быть особенно осторожными, поскольку в Go итерационные переменные повторно используются для каждой итерации (исходный код):
type T struct { name string } func main() { names := []T{{"foo"}, {"bar"}, {"baz"}} for _, name := range names { fmt.Printf("%p\n", &name) } }
Выход:
0x40c138 0x40c138 0x40c138
Теперь, когда мы узнали о замыканиях и тонкостях переменных range clause, давайте перейдем к вызовам методов.
x.m()
Такой вызов действителен, если набор методов типа x содержит m. Что такое метод se произвольного типа T?
- Набор методов T содержит все методы с типом приемника T.
- Набор методов типа указателя * T - это набор всех методов, объявленных с помощью получателя * T или T.
Да, здесь есть асимметрия. В спецификации языка есть дополнительное правило:
Ifx
is addressable and&x
's method set containsm
,x.m()
is shorthand for(&x).m()
:
Например, элементы карты не адресуются - https://play.golang.org/p/isfZwRiIL2a.
Это дополнительное правило актуально и в нашем случае. Что это на самом деле означает? Давайте проанализируем фрагмент сверху (исходный код):
type T struct { name string } func (t *T) M() { fmt.Println(t.name) } func main() { for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { defer v.M() } }
Это эквивалентно:
func main() { for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { defer (&v).M() } }
Теперь должно быть немного яснее. В примерах, где мы видим повторяющиеся строки, вызываются методы указателей на переменные итераций. Go не переопределяет переменные итерации - они используются повторно. Добавим еще немного логирования (исходный код):
for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { fmt.Printf("%p\n", &v) defer (&v).M() }
Выход:
0x40c138 0x40c138 0x40c138 baz baz baz
Метод вызывается по указателю, и для каждой итерации мы используем один и тот же указатель (ссылающийся на ячейку памяти переменной v
). Отложенные функции запускаются после завершения внешней функции, поэтому v
будет установлен на последний элемент среза - {"baz"}
. Вот почему в stdout мы видим baz
три раза.
Чем это отличается от случая, когда мы принимаем значения?
type T struct { name string } func (t T) M() { fmt.Println(t.name) } func main() { for _, v := range []T{{"foo"}, {"bar"}, {"baz"}} { defer v.M() } }
Теперь мы не используем указатели, а откладываем метод для копии каждого элемента среза - v
содержит копию {"foo"}
во время первой итерации, {"bar"}
во время второй итерации и так далее. Вот почему у нас нет дубликатов.
Примеры с операторами go почти такие же. Единственная разница в том, что планировщик может запускать методы в другом порядке, нежели прохождение среза.
👏👏👏 👏👏 ниже, чтобы помочь другим узнать эту историю. Пожалуйста, подпишитесь на меня здесь или в Твиттере, если вы хотите получать новости о новых сообщениях или ускорять работу над будущими историями.