Методы приемника указателя и переменные итерации

Еще одна проблема с повторным использованием переменных в предложении диапазона.

Этот рассказ начнется с загадки (исходный код):

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.

Да, здесь есть асимметрия. В спецификации языка есть дополнительное правило:

If x is addressable and &x's method set contains m, 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 почти такие же. Единственная разница в том, что планировщик может запускать методы в другом порядке, нежели прохождение среза.

👏👏👏 👏👏 ниже, чтобы помочь другим узнать эту историю. Пожалуйста, подпишитесь на меня здесь или в Твиттере, если вы хотите получать новости о новых сообщениях или ускорять работу над будущими историями.

Ресурсы