Гарантирован ли порядок данных в мапах Go
Что проверяют
Знаете ли вы, что порядок обхода мапы не просто не определён, а намеренно перемешивается рантаймом, и как получить детерминированный вывод. Следом часто спрашивают, зачем так сделали и что будет при конкурентной записи в мапу.
Как отвечать
- спецификация: порядок итерации по
mapне задан и не обязан совпадать между двумя обходами. Даже в одном запуске программы дваfor rangeподряд по той же мапе могут выдать ключи по-разному; - рантайм: стартовая позиция обхода выбирается случайно при каждом
range. Это сделано специально, чтобы никто не завязывался на порядок конкретной реализации. Благодаря этому в Go 1.24 мапы смогли перевести на Swiss tables, не ломая корректный код; - случайное зерно хэша: у каждой мапы оно своё, это ещё и защита от атак подбором коллизий;
- изменение при обходе: удалять ключи внутри
rangeбезопасно, а ключ, добавленный во время обхода, может попасть в итерацию, а может и нет.
Как получить порядок: собрать ключи и отсортировать. С Go 1.23 это одна строка:
for _, k := range slices.Sorted(maps.Keys(m)) {
fmt.Println(k, m[k])
}Если нужен порядок вставки, храните рядом срез ключей. Полезная деталь: fmt.Println и encoding/json сортируют ключи мапы при выводе, поэтому печать выглядит упорядоченной, хотя обход таким не является.
Где спотыкаются
Говорят, что порядок меняется между запусками. Он меняется и между двумя циклами в одной программе, поэтому тест, сравнивающий результат обхода со строкой, будет то проходить, то падать.
Путают мапу Go с dict в Python 3.7+ или LinkedHashMap в Java, которые помнят порядок вставки. В стандартной библиотеке Go такой структуры нет.
Делают выводы по опыту: «у меня три раза подряд вышло одинаково». Совпадение на нескольких запусках ничего не доказывает, гарантии в спецификации нет, и код, опирающийся на такой порядок, сломается в самый неудобный момент.