В какой версии Go был исправлен баг с указателями в цикле
Что проверяют
Знаете ли вы, как менялась семантика переменной цикла в Go, и понимаете ли, почему старый код вёл себя именно так. Следом почти всегда дают код с горутинами или &v внутри range и просят сказать, что он выведет в разных версиях.
Как отвечать
Ответ: Go 1.22, февраль 2024 года. Строго говоря, это не исправление бага, а изменение семантики языка: до 1.22 переменная цикла была одна на весь цикл и перезаписывалась на каждой итерации, с 1.22 на каждую итерацию создаётся новая. Касается и for i := 0; i < n; i++, и for k, v := range.
Что ломалось раньше:
- указатели:
ptrs = append(ptrs, &v)в цикле собирал указатели на одну и ту же переменную, и после цикла все смотрели на последнее значение; - замыкания и горутины:
go func() { fmt.Println(v) }()печатал не то, что ожидалось, чаще всего последнее значение несколько раз; - обход: писали
v := vили передавали значение параметромgo func(v int) {...}(v).
Важные детали:
- включение по go.mod: новая семантика действует для пакетов, где директива
goвgo.modравна 1.22 или выше. Старый модуль, собранный новым тулчейном, ведёт себя по-старому; - превью: в Go 1.21 её можно было включить через
GOEXPERIMENT=loopvar; - копии больше не нужны:
v := vстал лишним, линтерcopyloopvarв golangci-lint такие строки находит, а проверкаloopclosureвgo vetдля файлов с версией 1.22+ больше не срабатывает.
Для поиска кода, который поменял поведение при переходе (чаще всего это тесты, неявно полагавшиеся на старую семантику), команда Go сделала инструмент bisect.
Где спотыкаются
Называют другую версию, например 1.16 или 1.21. В 1.21 было только экспериментальное включение, по умолчанию изменение пришло в 1.22.
Думают, что достаточно обновить Go на машине. Решает версия в go.mod, и в проекте с go 1.20 старая семантика остаётся.
Считают, что теперь каждая итерация стоит дорого. Компилятор выносит переменную в кучу, только если её адрес убегает из итерации, в остальных случаях она остаётся на стеке, как и раньше.