Что лучше использовать для хранения данных по пользователям: двусвязный список с мапой или один общий список
Что проверяют
Видите ли вы за формулировкой стандартную конструкцию, индекс поверх контейнера, и умеете ли считать стоимость операций под конкретный профиль нагрузки, а не повторять, что «мапа всегда быстрее».
Как отвечать
Сначала уточнить операции: поиск по идентификатору, обход в порядке добавления, удаление из середины, вытеснение старых записей. Дальше сравнение:
- мапа вместе с двусвязным списком даёт O(1) на поиск, вставку, удаление и перенос элемента в начало. Это ровно устройство LRU-кэша:
map[string]*list.Elementповерхcontainer/list, причём в значении узла хранят и ключ, чтобы при вытеснении хвоста удалить запись из мапы; - один срез: доступ по индексу O(1), но поиск по значению O(n), а удаление из середины O(n) на сдвиг. Зато аллокаций минимум и данные лежат подряд, поэтому на сотнях элементов линейный проход по срезу обгоняет мапу;
- память: каждый узел списка это отдельная аллокация и два указателя, у среза накладных расходов нет.
Вывод по условиям: нужны точечный доступ и удаление из произвольного места, берите мапу со списком; данных мало и они в основном перебираются целиком, берите срез.
Где спотыкаются
Кладут в мапу индексы среза. После удаления одного элемента все индексы правее съезжают на единицу, и мапа начинает указывать на чужие записи, причём молча.
Второе: удаляют узел из списка и забывают запись в мапе. Сборщик мусора не тронет узел, пока на него есть ссылка, и это утечка, которая видна только на долго живущем процессе.
Третье: считают, что delete возвращает память. Мапа в Go не отдаёт свои корзины обратно, и при потоке вставок с удалениями она растёт; при таком профиле её периодически пересоздают. И встроенной потокобезопасности у мапы нет, конкурентный доступ закрывается sync.RWMutex.