Представьте обход среза структур: изменение поля переменной элемента не меняет исходный срез. Какой механизм Go это объясняет?
Переменная элемента в range получает копию значения элемента среза. Поэтому изменение её полей меняет только копию, а не структуру внутри исходного среза.
Чтобы изменить сам элемент среза, нужно обращаться к нему по индексу либо хранить в срезе указатели и изменять объект, на который они указывают.
Конструкция range предназначена для краткого и единообразного обхода коллекций без ручного управления индексами. Она предоставляет значения элементов через переменные цикла, что удобно для чтения и не требует неявного изменения исходной коллекции.
Такое поведение согласуется с базовой семантикой Go: присваивание значения структуры создаёт её копию, а не ссылку на исходный объект.
Ошибка возникает, когда разработчик воспринимает переменную элемента как ссылку на элемент среза. Программа успешно компилируется, но изменения незаметно теряются, что особенно опасно при нормализации данных, обновлении статусов или подготовке объектов к записи.
Риск отсутствует при чтении элементов. Он появляется именно тогда, когда через переменную range пытаются изменить поля структуры.
На каждой итерации Go извлекает элемент и присваивает его значение переменной цикла. Для структуры это копирование всех полей. Если структура содержит ссылочные компоненты, например срез, map или указатель, копируются сами ссылки, но не обязательно данные за ними.
В первом цикле изменяется копия user. Во втором цикле индекс используется для обращения к реальному элементу среза.
Если срез содержит указатели, копируется указатель, поэтому изменение объекта через него будет видно из исходного среза. Однако переназначение самой переменной-указателя всё равно не заменит указатель в срезе.
Компромисс прост: обход по значению безопаснее и удобнее для чтения, а обход по индексу необходим для изменения элементов. Для крупных структур копирование также может иметь стоимость, поэтому при частой обработке следует оценивать размер элементов и при необходимости использовать указатели, учитывая дополнительные расходы на косвенный доступ и управление памятью.
В обработчике пользователей нужно пометить просроченные записи как неактивные. Вариант с изменением переменной range не сработает: после цикла исходный срез останется прежним.
Можно создать новый срез и добавлять в него изменённые копии. Это явно отделяет исходные данные от результата, но требует дополнительной памяти и копирования всех элементов.
Можно использовать срез указателей. Тогда поля объектов изменяются напрямую, однако усложняются владение объектами, жизненный цикл данных и анализ возможных побочных эффектов.
Для обычного среза структур оптимален обход по индексам: он изменяет нужные элементы без дополнительного среза и без изменения модели данных. Такой вариант был бы выбран для обработчика, если не требуется сохранять исходную версию коллекции.
Меняется ли элемент, если структура содержит срез или map?
Поля структуры копируются поверхностно. Сам срез или map внутри структуры передаётся как значение, содержащее ссылку на внутренние данные. Поэтому изменение элементов вложенного среза или map через переменную range обычно видно в исходной структуре, а присваивание нового среза или map меняет только копию поля.
Почему добавление элементов в срез внутри range не гарантирует их обход?
Для среза цикл использует длину, определённую при начале обхода. append может изменить длину локальной переменной среза и даже выделить новый массив, но это не расширяет заранее установленное число итераций. Поэтому новые элементы не становятся автоматически частью текущего обхода.
Как изменить структуру в срезе, если нужен одновременно индекс и значение?
Нужно использовать индекс как средство записи: значение можно читать через переменную цикла, а изменять — через обращение к элементу среза по индексу. Это сохраняет удобство range и устраняет ошибку изменения копии. Если структура большая, можно не копировать её вовсе и работать непосредственно с элементом по индексу.