В Go конструкция цикла for может включать блок инициализации (init), проверки условия и постфиксного выражения. Исторически такой механизм создан для удобства написания кода и привычки C-подобных языков. Однако в Go область видимости переменной цикла (i) имеет специфику, которая сильно влияет на поведение внутри вложенных функций, замыканий (closures) и горутин.
Проблема — при запуске горутин или замыканий на каждой итерации цикла часто возникает неожиданное поведение: переменная i не копируется, а "захватывается" по ссылке, то есть замыкание обращается к общей переменной цикла, которая после окончания цикла принимает последнее значение. Это приводит к одинаковому результату во всех горутинах/closure, хотя логика могла предполагать иное.
Решение — при необходимости передавать значение переменной каждой итерации, используйте явное копирование переменной (через дополнительную переменную) или передавайте ее как аргумент в замыкании.
Пример кода:
for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // Правильно! Копированное значение } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Ошибка: все горутины напечатают 3 }
Ключевые особенности:
Меняется ли область видимости переменной for при использовании break или continue?
Нет. Область видимости переменной, объявленной в for, всегда ограничена блоком этого цикла. Break или continue только прерывают очередную итерацию, но не "прокидывают" переменную наружу.
Можно ли захватить переменную, объявленную в init-части for, внутри метода вне цикла?
Нет. Переменная видна только внутри самого for и всех вложенных в него блоков, но не вне его после завершения цикла.
Что произойдёт, если захват переменной произойдет в defer-выражении внутри for?
Та же ситуация: defer-функция "увидит" не значение в момент создания, а текущее значение переменной к моменту выполнения defer (обычно — последнее значение цикла).
for i := 0; i < 3; i++ { defer fmt.Println(i) // все defer напечатают 3 }
В веб-сервере Go разработчик запускал несколько горутин для обработки различных портов, используя индекс порта как переменную цикла и непосредственно захватывая ее в лямбда-выражение. Все горутины обращались к одному порту — последнему в массиве.
Плюсы:
Минусы:
В команде ввели правило — всегда копировать значение переменной цикла в новую переменную, которую уже захватывает closure/goroutine.
Плюсы:
Минусы: