Давайте разберем случай.
Сначала давайте обратимся к документам по testing.T.Run
:
Выполнить прогоны f как подтест t, называемый имя. Он запускается f в отдельной goroutine <…>
(выделение мое.)
Так что, когда вы звоните t.Run("some_name", someFn)
, это SomeFn
запускается набором тестов, как если бы вы вручную делали что-то вроде
go someFn(t)
Далее, давайте заметим, что вы не передаете именованную функцию при вызове t.Run
, а скорее передаете ее так function literal; давайте процитируем язык spe c на них :
Функциональные литералы замыкания: они могут ссылаются на переменные, определенные в окружающей функции. Затем эти переменные распределяются между окружающей функцией и литералом функции и сохраняются до тех пор, пока они доступны.
В вашем случае это означает, что когда компилятор компилирует тело литерала функции, она заставляет функцию «закрывать» любую переменную, о которой упоминает ее тело, и которая не является одним из параметров формальной функции; в вашем случае единственным параметром функции является t *testing.T
, следовательно, любая другая переменная, к которой осуществляется доступ, захватывается созданным замыканием.
В Go, когда литерал функции закрывается над переменной, он делает это, сохраняя ссылка на эту переменную - которая явно упоминается в spe c как («Эти переменные затем разделяются между окружающей функцией и литералом функции <…>», снова , выделено мое.)
Теперь обратите внимание, что циклы в Go повторно используют переменных итерации на каждой итерации; то есть, когда вы пишете
for _, v := range apps {
, эта переменная v
создается один раз во "внешней" области видимости l oop и затем получает переназначенный на каждой итерации л oop. Напомним: той же переменной, чье хранилище находится в некоторой фиксированной точке в памяти, присваивается новое значение на каждой итерации.
Теперь, поскольку литерал функции закрывает внешние переменные, сохраняя ссылки для них - в отличие от копирования их значений во «время» его определения в , - без этого «навороченного» * 1057 * «трюка» каждого литерала функции, созданного при каждом вызове t.Run
в вашем l oop будет указываться точно такая же итерационная переменная v
из l oop.
Конструкция v := v
объявляет другую переменную с именем v
, которая является local для Тело l oop и в то же время присваивает ему значение итерационной переменной l oop v
. Поскольку локальный v
"shadows" l oop итератора v
, объявленный впоследствии литерал функции будет закрываться по этой локальной переменной, и, следовательно, каждый литерал функции, созданный на каждой итерации, будет закрываться по отдельной отдельной переменной v
.
Зачем это нужно, спросите вы?
Это необходимо из-за тонкой проблемы с взаимодействием итерационной переменной l oop и goroutines, которая подробно описана на Go wiki : когда кто-то делает что-то вроде
for _, v := range apps {
go func() {
// use v
}()
}
Создается литерал функции, закрывающий v
, и затем он запускается с помощью оператора go
- параллельно с goroutine, который запускает l oop, а все остальные goroutines запускаются на других len(apps)-1
итерациях.
Эти goroutines, использующие наши функциональные литералы, ссылаются на один и тот же v
, и поэтому все они имеют гонку данных по этой переменная: программа, выполняющая lo oop , записывает в нее , а функции, выполняющие литералы функции read * 10 86 * из него - одновременно и без какой-либо синхронизации.
Надеюсь, к настоящему времени вы должны увидеть, как кусочки головоломки собираются вместе: в коде
for _, v := range apps {
v := v
t.Run("", func(t *testing.T) {
expectedOutput = `=` + v + `
// ...
литерал функции передан t.Run
закрывается за v
, expectedOutput
, cmpOpts.SingleApp
(и может быть что-то еще), а затем t.Run()
заставляет эту функцию буквально запускаться в отдельной процедуре, как задокументировано, - создавая гонку данных classi c на expectedOutput
и cmpOpts.SingleApp
и все остальное, кроме v
(fre * 1121) * переменная на каждой итерации) или t
(передается вызову функции literal).
Вы можете запустить go test -race -run=TestIntegrationAppsWithProductionSelf ./...
, чтобы увидеть, как задействованный детектор гонки разбивает код вашего тестового примера.