Как работает перебор мапы в цикле в Go и является ли он случайным
Что проверяют
Знаете ли вы, что порядок обхода мапы не задан спецификацией и рантайм перемешивает его намеренно, а не «из-за хешей». Следом обычно просят вывести мапу в стабильном порядке или спрашивают, можно ли так выбирать случайный элемент.
Как отвечать
- спецификация: порядок
for rangeпо мапе не определён и не обязан совпадать между двумя проходами. Опираться на него нельзя даже в тестах; - реализация: при каждом
rangeрантайм выбирает случайную стартовую позицию. Так было со старыми бакетами и осталось в Swiss Table, на которую мапы перешли в Go 1.24. Порядок и так меняется при росте таблицы, а перемешивание не даёт коду случайно на него завязаться. Отдельно у каждой мапы свой случайный seed хеш-функции, это защита от подбора коллизий; - это не генератор случайности: распределение далеко от равномерного, соседние элементы часто идут подряд. Для случайного выбора есть
math/rand/v2; - стабильный порядок: соберите и отсортируйте ключи. С Go 1.23 это одна строка
slices.Sorted(maps.Keys(m)), дальше обход по срезу.fmt.Printlnпечатает мапу с отсортированными ключами, иencoding/jsonсортирует их при маршалинге, но сама мапа от этого не упорядочивается; - изменение во время обхода: удалённый и ещё не пройденный ключ в цикл не попадёт, а добавленный может попасть, а может нет. Спецификация это разрешает, но результат недетерминирован.
Где спотыкаются
Говорят, что порядок случаен, потому что «хеши меняются при каждой итерации». Хеш ключа в пределах мапы постоянен, меняется только точка, с которой начинается обход.
Пишут тест, который сравнивает результат обхода с ожидаемым срезом. Локально на трёх элементах он может проходить и падать в CI. Правильно сортировать результат или сравнивать как множества.
Забывают про конкурентность: чтение и запись мапы из разных горутин без sync.Mutex может уронить программу с fatal error: concurrent map iteration and map write. Это не паника, recover её не перехватит.