Переменная цикла до 1.22
В «Полке» жил древний баг: фоновый логгер в цикле по файлам писал в журнал один и тот же путь. Миша взялся за разбор и к вечеру нашёл причину в старом коде, который никто не трогал годами.
до Go 1.22 все итерации for делили одну переменную цикла: последнее значение гарантированно видели только замыкания, вызванные после цикла, а горутина, стартующая внутри цикла, могла увидеть промежуточное — и это гонка данных. Лечилось v:= v или передачей аргументом.
Замыкания в цикле
До Go 1.22 в for i:= 0; i < 3; i++ переменная i была одна на весь цикл. Замыкания, захватившие её, после цикла видели последнее значение:
Две классические починки
Первый способ — скопировать переменную в теле цикла: i:= i вводит новую переменную на каждой итерации.
for i := 0; i < 3; i++ {
i := i
funcs = append(funcs, func() int { return i })
}
Второй — передать значение аргументом: аргумент копируется при вызове.
for i := 0; i < 3; i++ {
funcs = append(funcs, func(n int) func() int {
return func() int { return n }
}(i))
}
С for range история та же: до 1.22 одна переменная v на все итерации — последний элемент гарантированно видели только замыкания, вызванные после цикла, а захват в горутине мог дать промежуточный элемент, и это гонка данных.
Почему это вообще было багом
Семантика была такой с первой версии языка и жила больше десяти лет, но каждый год ломала новичков — баг был настолько частым, что в 1.22 поведение поменяли. Логгер «Полки» — живое подтверждение: код, написанный по старым правилам, годами писал в журнал не тот путь. Об этом — следующий урок.
Вы узнаете:
- почему
&iдо 1.22 — одна и та же ячейка - как
rangeпо слайсу до 1.22 копировал заголовок
До 1.22: одна переменная цикла на все итерации; последнее значение видели замыкания, вызванные после цикла, а горутина внутри цикла — промежуточное, в гонке данных. Починки: i := i или аргумент.
10 000 ₽ · 60 минут · онлайн