Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп всего цикла, а не одной его итерации, то в 1.22 это поведение поменяют.
проще показать на примере:
funcs := []func(){}
for i := 0; i < 5; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
funcs[0]()
Последняя строка примера напечатает 5 в go 1.21, но в go 1.22 будет уже интуитивно понятный 0.
С одной стороны, это нарушение обратной совместимости, но зато не надо писать пугающее новичков i := i для починки скоупа.
На самом деле, сложно представить кейс, чтобы кто-то хотел во все функции замкнуть именно последнее значение цикла. В тоже время такая неинтуитивная ситуация, как сейчас, регулярно выстреливает в ногу, вот пример реального бага в Lets Encrypt:
// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a
// protobuf authorizations map
func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) {
resp := &sapb.Authorizations{}
for k, v := range m {
// Make a copy of k because it will be reassigned with each loop.
kCopy := k
authzPB, err := modelToAuthzPB(&v)
if err != nil {
return nil, err
}
resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{
Domain: &kCopy,
Authz: authzPB,
})
}
return resp, nil
}
Здесь разработчик скопировал переменную k, а вот v — уже забыл. В итоге функция modelToAuthzPB получила указатели на одну и ту же переменную.
Новое поведение языка Go можно включить уже в 1.21 с помощью переменной окружения GOEXPERIMENT=loopvar
и протестировать вашу программу. В любом случае, переход с 1.21 на 1.22 надо будет делать осторожно, возможно у вас что-то сломается. А может, наоборот, заработает (смайлик).
Если хотите больше новостей и полезной информации о разработке, подписывайтесь на мой tg-канал Cross Join
Комментарии (60)
nightlord189
20.09.2023 09:46+31Прощай старый добрый способ докопаться на собесах)
zergon321
20.09.2023 09:46+3Я вот не понимаю, почему на собеседованиях спрашивают подобный кринж. Вот джаваскриптеров вообще любят спросить о том, что выведет лютая мешанина из разных скобок
Lexicon
20.09.2023 09:46+14На собесах хорошо отлавливает джунов
Если человек помнит или хотя бы подзабыл, перед вами джун, если прокричал что-то на орочьем, достал топор и сближается, значит про опыт не врет
mapcuk
20.09.2023 09:46+1По-моему такой вопрос реально отражает опыт, я сам с этим поведением for ... range cтолкнулся.
Меня больше смущают вопросы про поведение, если писать (или читать) в закрытый канал (буфферизованный), если так приходится делать - это уже плохой код. Зачем даже задуматься о том, какое будет поведение?
a_che
20.09.2023 09:46на самом деле мне интересно, как это будет сделано.
сейчас создается одна переменная с скоупом в весь цикл, и дальше в нее копируются данные на каждую итерацию.
а будет как? на каждую итерацию будет новая переменная?
звучит очень дорого, а если в цикле миллион итераций?
KivApple
20.09.2023 09:46+4Звучит бесплатно, если речь о примитивах.
Ссылка на примитив как и любая другая ссылка весит 32 или 64 бита в зависимости от разрядности. Значение int весит 32 бита. В замыкание неизбежно что-то копировать да придётся - либо ссылку, либо значение. Копировать значение примитива стоит не дороже ссылки, а иногда дешевле (если у нас 64 битная ОС, а примитив 32 бита). Также чтение примитива по значению точно быстрее, чем по ссылке, потому что примитив читается за одну операцию, а примитив за ссылкой за две (сначала прочитать ссылку, потом значение по адресу из неё). Наконец, оптимизатор, зная что примитив никто за пределами лямбды не изменит, может лучше оптимизировать код.
В языках типа C++, где ссылки создаются более явно, давно есть правило, что примитивы по ссылке передают только если очень надо. По значению эффективнее. По ссылкам хорошие программисты передают объекты, которые занимают в памяти больше размера 2-3 указателей (никакие примитивы столько не весят), либо если нужны особые свойства ссылок (возможность менять из нескольких мест и т. д.)
unreal_undead2
20.09.2023 09:46Насколько понимаю, семантически захват в Go всегда по ссылке и просто скопировать значение в общем случае не получится. Скажем, два захвата на одной итерации должны получить одну общую переменную.
KivApple
20.09.2023 09:46Новость об изменении этого поведения как раз. А мой комментарий о том, что для примитивов это повысит эффективность, а не понизит. Если примитивы в Go как в Java (выделяются не в куче и не имеют таблицы виртуальных методов), а не как в Python.
unreal_undead2
20.09.2023 09:46Новость об изменении этого поведения как раз
Новость о том, что на разных итерациях разные переменные. Но внутри каждой итерации переменная одна, никто не мешает захватить её в теле цикла несколько раз в разные closure.
WASD1
20.09.2023 09:46да не копируем итератор итератор в отдельную переменную на каждой итерации (и замыкаемся по ссылке на копию).
Если копией не воспользовались - всё отлично DCE её легко удалит.
ris58h
20.09.2023 09:46+4Думаю, что если нет захвата переменной в замыкание, то можно и не создавать лишнего.
WASD1
20.09.2023 09:46А как сейчас в Го продляется жизнь замкнутых переменных со стека?
Через двойной указатель и копирование в кучу при выходе из скоупа?
Ну и теоретически можно предусмотреть отдельную машинерию только если замыкание захватывает переменную цикла.Upd: если в го есть понятие "объект расположенный на стеке" (с конструкторами \ деструкторами которые компилятор умеет элиминировать если они пустые) - то даже специальный случай вроде не потребуется.
neolink
20.09.2023 09:46Она не продляется, для этого есть escape analysis - если он говорит что значение переживает функцию оно сразу аллоцируется в куче
WASD1
20.09.2023 09:46Спасибо.
Тогда моё update наверное не верно. Если у примитивных типов нет технического деструктора (вызываемого language-runtime, в абсолютном большинстве случаев не вызываемого), не на что навесить нужную логику (вводить его сейчас, понятно, поздновато).
И значит скорее всего надо отдельно делать циклы без замыканий (ничего не менять) \ отдельно циклы где замыкания захватывают переменную цикла.
WASD1
20.09.2023 09:46Копируете переменную цикла. Замыкаетесь по копии переменной. Удаляете ненужные скопированные переменные (на стеке DCE справится, а вот если сделали выделение памяти - надо уже ручками удалять).
ПС
Желательно эти 3 фазы поставить подряд.
gandjustas
20.09.2023 09:46+2Если анализ гитхаба и другого опенсорса говорит, что ничего не сломается от такого изменения, то ничего страшного.
buldo
20.09.2023 09:46+5Ломающее изменение и подъем только минорной версии языка? Что-то я не понимаю в semver
Daddy_Cool
20.09.2023 09:46+3"Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп".
... Им это слово много говорило. Жаргон это конечно хорошо, но все же...
Даже ПЕРВЫЙ ЖЕ коммент использут нормальное слово "видимость".varanio Автор
20.09.2023 09:46+1скоуп уже давно общепринятый термин. Ну и кстати...
жаргон - это французское слово
коммент - это английское словоDaddy_Cool
20.09.2023 09:46Троллинг засчитан. ;)
"скоуп уже давно общепринятый термин". Сорри (анлийское слово), но нет.
gravyzzap
20.09.2023 09:46Scope часто переводят как область видимости. Но ключевое слово — область. "Видимость" в первом комментарии в контексте статьи не вызывает вопросов, но в другом контексте может вызывать.
Если бы можно было всем договорится о "правильном" переводе, я бы выбрал "ареал".
lanseg
20.09.2023 09:46О, я с таким сталкивался, совершенно не ожидая такого поведения. Подумал "фу" и обернул тело цикла в вызов функции, совсем как в js когда-то
Conacry
Интересно почему изначально была выбрана видимость per-loop, а не per-iteration? Для экономии памяти?
varanio Автор
наверно, просто не продумали этот момент, сделали как проще
1755
Вы прям идеально описали мои мысли, которые то тут то там возникали при освоении Go. Простой как валенок, в этом его прелесть, но к некоторым моментам было ощущение как вы описали.
zahnah
Ну, не думаю, что на этом ресурсе найдётся кто-то, кто сможет их осудить. Все мы своего рода мэйнтэйнеры Go.
red75prim
Традиция. Во многих языках есть (или было) такое же поведение: питон, ява, C#, javascript. По-отдельности реализация циклов и лямбд выглядит естественной, но их взаимодействие даёт неочевидное последствие.
ris58h
Будьте любезны, пример. Что-то придумать не могу, учитывая что в замыкание в Java можно захватить только final или effectively final переменную.
red75prim
Да, яву я зря добавил. Там компилятор заставляет копировать переменную цикла, чтобы её захватить в замыкание.
vic_1
Именно так, а go в мусорное ведро
rmrfchik
В java не так (там нельзя передавать не effectively final). В javascript не так (всё нормально захватывается). В python свободная переменная передаётся по имени, это вообще что-то из 60-х и алгола, но это понятное поведение.
c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.
Дурость с for я видел только в Go. К сожалению, язык миновал стадию проектирования и сразу ушёл в продакшен.
red75prim
Вот прямо сейчас запустил в браузере:
Результат:
Было как в javascript, потом пофиксили (нет, не так как сделано в java, где запрещено захватывать переменную цикла). Тут в комментариях уже про это писали.
Format-X22
Вы в курсе что с 2014 года уже не используется var? Как минимум в хроме, остальные подтянулись к 2016. То есть минимум 7 лет как проблемы нет.
KivApple
С let другое поведение?
ris58h
Да. Можно сказать, что он для этого и создавался - чтобы у переменной была понятная и ожидаемая (с точки зрения обычного человека) область видимости.
red75prim
Цитирую себя: "Во многих языках есть (или было) такое же поведение"
В js, оказывается, и есть и было. Хорошо, буду знать, спасибо.
vanxant
Просто добавлю, что
let
был в фаерфоксе со времён первой браузерной. Но сначала его принципиально не хотел понимать ИЕ, потом хром. Потом разработчики хрома таки что-то поняли.mvv-rus
Ну, разработчики IE тоже поняли, только поздновато — в 11-й версии ;-) И то не во всех режимах.
Была у меня с IE такая вот хохма во дни минувшие. Делаю модуль расширения для ADFS на Win2012 R2. Ему там положено возвращать фрагменты HTML, которые ADFS вставляет в свой шаблон и возвращает получившуюся страницу пользователю. Проверяю работу в IE11- let в скриптах в фрагментах не работает. А те же самые фрагменты, вставленные в статический файл HTML — копию возвращаемой страницы — работают на ура. Сперва поофигевал, потом разобрался: AD FS передавал в IE заголовок, включающий режим совместимости с IE10 — а в том режиме let предусмотрен не был.
Shagrat2
Хорошо когда клиенты обновляют свое ПО и компьютеры.
У меня 20% клиентов использует IE
unreal_undead2
В С++ никаких новых переменных на каждой итерации не создаётся. Но там захватывать переменную цикла по ссылке - уже взвод курка для выстрела в ногу.
bogolt
В плюсах такой проблемы нет, из-за другой модели работы с памятью. Если мы захватим переменную по указателю то по завершении цикла мы получим dangling pointer. В го же из-за автоматического управления памятью так сделать можно без того чтобы получить ошибку доступа к памяти.
unreal_undead2
Теоретически можно захватить счётчик по ссылке на одной итерации и использовать полученную лямбду на другой - это и в С++ валидно (и как раз такой код будет ломаться от описанного изменения в Go). Другое дело, что большого практического смысла нет.
omaxx
в питоне такое же поведение:
vadimr
В питоне-то понятно, почему: тело функции (в данном случае лямбды) не интерпретируется до момента её вызова. Там любой лексически верный мусор можно написать между : и for.
omaxx
А что вы хотели показать этим примером? Это вполне ожидаемое поведение, если нет локальной переменной с таким именем, то питон будет искать среди глобальных переменных (вернее Enclosing, Global, Built-in). Ну и при этом на момент объявления функции эта переменная существовать не обязана.
vadimr
Ну да, так всё и есть.
Я хотел показать именно то, что странно было бы ожидать, что значение i будет меняться в элементах списка func на каждой итерации, если оно фактически востребуется только один раз в момент вызова функции в последней строке.
Для такого поведения, как в новом Go, в питоне нужно было б заводить отдельный контекст для каждой итерации цикла, что обессмыслило бы само понятие перементой цикла.
omaxx
можно вот так сделать:
sergeaunt
Это лучшая фраза, которую я видел на Хабре за последний год.
kasthack_phoenix
Там loop-level в
for
, а вот вforeach
зависит от версии языка: изначально было loop-level, но в C# 5 поменяли на iteration-level.ivan_mariychuk
Об этом было очень важно упомянуть.
rmrfchik
Посчитал, что важно. Чтобы отмести предложения сделать это разными способами.
ivan_mariychuk
Понял, извините.
outcatcher
В Python у циклов (как и у if) вообще нет своей области видимости, можно переменную вообще первый раз внутри цикла присвоить, и снаружи потом использовать. Если не присвоишь - просто будет
NameError
arheops
Вы еще спросите, почему в JS видимость this везде разная.
Просто провтыкали, как обычно.