Какая проблема возникает с анонимными функциями и переменными в цикле в Go
Что проверяют
Знаете ли вы классическую ловушку с замыканием в цикле и, главное, что в Go 1.22 поведение изменилось. Следом обычно спрашивают, от чего зависит, какая семантика включится в конкретном проекте, и остались ли случаи, где проблема жива.
Как отвечать
Замыкание в Go захватывает переменную, а не её значение на момент создания функции. До Go 1.22 переменная цикла for i := 0; i < 5; i++ была одна на весь цикл, поэтому функции, собранные в срез, после цикла печатали 5 5 5 5 5. С горутинами go func() { fmt.Println(i) }() было ещё хуже: вывод зависел от планировщика, плюс гонка данных, которую ловит go test -race.
Лечили копией i := i в теле цикла или передачей значения параметром: go func(i int) {...}(i).
Начиная с Go 1.22 каждая итерация и трёхчастного for, и for range получает собственную переменную, и тот же код печатает 0 1 2 3 4. Важно, от чего это зависит:
- директива
goвgo.mod: новая семантика действует для модулей, где указана версия 1.22 или выше. Старый модуль, собранный новым компилятором, ведёт себя по-старому; i := iстал лишним: линтерcopyloopvarв golangci-lint находит такие копии и предлагает их удалить;- захват остаётся захватом: если переменная объявлена снаружи цикла или вы меняете переменную цикла после создания замыкания, функция увидит последнее значение.
Хороший ответ заканчивается мыслью, что правило «замыкание видит переменную, а не снимок» никуда не делось, изменилась только область жизни переменной цикла.
Где спотыкаются
Рассказывают только про старое поведение и говорят «в Go всегда выведется 5 5 5». Для современного проекта это неверно, и интервьюер это знает.
Говорят, что замыкание захватывает «по ссылке, как указатель». Точнее сказать, что оно разделяет саму переменную с окружающим кодом. Указатели тут ни при чём, и такая формулировка путает, когда разговор переходит на срезы и мапы.
Считают, что переход на 1.22 решил всё, и забывают про синхронизацию горутин. Каждая горутина теперь видит своё i, но без sync.WaitGroup или errgroup функция main может завершиться раньше, чем они что-то напечатают.