Почему в Go нельзя взять значение по ссылке из мапы
Что проверяют
Понимаете ли вы, что такое адресуемость в Go и почему у мапы её нет. Вопрос отделяет тех, кто выучил фразу «мапа передаётся по ссылке», от тех, кто представляет устройство рантайма.
Как отвечать
Начать с формулировки: элемент мапы не адресуем. &m[k] не компилируется, и для мапы структур m[k].Field = v тоже ошибка компиляции. Причина в реализации: мапа это набор бакетов, при росте рантайм выделяет новый массив бакетов и эвакуирует туда пары. Адрес значения меняется, и указатель, взятый до вставки, указывал бы в никуда. Со слайсом язык так делать разрешает: &s[i] валиден, потому что после реаллокации старый массив остаётся живым, а не освобождается под ногами.
Что делать вместо этого:
- хранить указатели,
map[string]*User, тогдаm[k].Name = "Bob"работает, потому что разыменовывается указатель, а не элемент мапы; - читать, менять копию, класть обратно:
u := m[k]; u.N++; m[k] = u; - для счётчиков хватает
m[k]++, компилятор сам разворачивает это в чтение и запись.
Отдельно стоит сказать, что фраза «мапа передаётся по значению» верна буквально: переменная типа map это указатель на хедер таблицы, копия переменной ссылается на ту же таблицу, поэтому функция, которая пишет в мапу, меняет её и у вызывающего. Но это свойство самой мапы, а не её элементов.
Где спотыкаются
Говорят «мапы в Go передаются по ссылке». Ссылочных типов в языке нет, передаётся копия значения, внутри которой лежит указатель на общую хеш-таблицу. Из этой неточности и вырастает путаница с элементами.
Второе: вариант с map[string]*User подают как бесплатное улучшение. Указатели дают отдельную аллокацию на каждый элемент, нагружают сборщик мусора и добавляют возможность разыменовать nil там, где раньше отсутствующий ключ давал нулевую структуру.
Третье: приплетают сюда гонки. Мапа не безопасна для конкурентной записи сама по себе, рантайм прямо падает с concurrent map writes, и адресуемость к этому отношения не имеет. Синхронизация нужна независимо от того, хранятся значения или указатели.