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