Какие особенности есть у мап в Go при удалении элементов
Что проверяют
Знаете ли вы, как устроена map и чем удаление отличается от освобождения памяти. Следом почти всегда спрашивают, можно ли удалять во время range и что будет при удалении из нескольких горутин.
Как отвечать
- крайние случаи:
delete(m, k)ничего не возвращает, отсутствие ключа не ошибка. Удаление изnil-мапы тоже безопасно, в отличие от записи, которая паникует; - чтение после удаления:
m[k]вернёт нулевое значение типа, поэтому отличить «удалён» от «лежит ноль» можно только формойv, ok := m[k]; - удаление во время
range: разрешено спецификацией, удалённый и ещё не пройденный элемент в обходе не появится. Добавленные во время обхода элементы могут появиться, а могут нет; - память не возвращается: мапа не сжимается. Слоты очищаются, и объекты, на которые ссылались значения, соберёт GC, но таблица остаётся того размера, до которого дорастала. Это верно и для реализации на Swiss Tables из Go 1.24. Чтобы вернуть память, создают новую мапу и копируют живые элементы;
clear(m): встроенная функция с Go 1.21, удаляет всё разом, ёмкость тоже сохраняет;- конкурентность: запись и
deleteиз нескольких горутин без синхронизации даютfatal error: concurrent map writes. Это не паника,recoverеё не перехватит. Нуженsync.Mutex,sync.RWMutexилиsync.Mapпод подходящий сценарий.
Сильная деталь напоследок: ключ NaN в map[float64]T через delete не удалить, потому что NaN не равен сам себе. clear с этим справляется, и это одна из причин, по которым её добавили.
Где спотыкаются
Ждут, что после массового удаления память вернётся. У кэша, который вырос в пике и потом опустел, потребление так и остаётся пиковым, пока мапу не пересоздадут.
Боятся удалять внутри range и сначала собирают ключи в отдельный срез. Ошибкой это не будет, но показывает, что спецификацию вы не открывали.
Путают гонку на мапе с обычной паникой и оборачивают код в recover. Процесс всё равно упадёт, а детектор в рантайме срабатывает не каждый раз, так что лечится это только синхронизацией.