ПрограммированиеMiddle/Senior Go backend разработчик

Как работают циклы for-range по срезам и map в Go и какие особенности копирования значений возникают при их использовании?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

Конструкция for-range появилась в Go как способ итерации по коллекциям (срезам, массивам, map, строкам). Разработчики Go внедрили оптимизацию: на каждой итерации цикла происходит копирование значения, а не прямое использование по ссылке, что может приводить к неочевидным ошибкам, особенно с переменными цикла.

Проблема:

Многие ошибаются при попытке взятия адреса переменной внутри range (например, &v), считая, что получают адрес элемента коллекции, а на самом деле получают адрес локальной переменной.

Решение:

В цикле for-range на каждой итерации создаются новые копии переменных итератора (key, value). Для простых типов это безболезненно, но для структур приводит к неожиданностям при сохранении указателя на элемент — он всегда будет указывать на одну и ту же переменную, а не на разные элементы среза.

Пример кода:

people := []Person{{Name: "Ivan"}, {Name: "Oleg"}} ptrs := make([]*Person, 0) for _, p := range people { ptrs = append(ptrs, &p) // все ptrs будут ссылаться на один и тот же p }

Ключевые особенности:

  • В каждой итерации цикла создаются новые копии переменных key/value
  • Взятие адреса value внутри for-range возвращает не адрес элемента в коллекции, а адресу временной переменной
  • Для map итерация проходит в произвольном порядке, нет гарантии последовательности

Вопросы с подвохом.

Что произойдет при сохранении ссылок на переменную value внутри range?

Все ссылки будут указывать на одну и ту же память, так как value — временная переменная.

for _, v := range someSlice { ptrs = append(ptrs, &v) } // Все ptrs содержат ссылку на одну и ту же переменную!

Можно ли изменить элемент коллекции по ссылке через value в range?

Нет, изменение value не затрагивает исходный элемент коллекции. Для изменения необходимо обратиться по индексу.

for _, v := range arr { v.Field = 10 // arr не изменится } for i := range arr { arr[i].Field = 10 // правильно }

Гарантирует ли for-range по map последовательный порядок обхода?

Нет, порядок итерации по map в Go не определён и может быть разным в каждом запуске приложения.

Типовые ошибки и анти-паттерны

  • Использование &value в range с целью сохранить ссылки на разные элементы
  • Изменение переменной value вместо обращения по индексу
  • Ожидание последовательного порядка обхода map

Пример из жизни

Негативный кейс

Разработчик пытается сериализовать список ссылок на элементы структуры через range и сохраняет &value в отдельный срез. Получается срез одинаковых адресов.

Плюсы:

  • Лаконичность цикла

Минусы:

  • Все ссылки одинаковы, при изменении одного элемента меняются «все»

Позитивный кейс

Итерируют по индексу и сохраняют указатель на нужный элемент массива:

for i := range arr { ptrs = append(ptrs, &arr[i]) }

Плюсы:

  • Каждый указатель указывает на отдельный элемент исходного среза

Минусы:

  • Синтаксис длиннее, но предсказуемый результат