Map в Go — хэш-таблица: ключ находится быстро, порядка обхода нет. Нулевая map паникует на записи. Конкурентная запись без блокировки — data race и часто падение процесса.
Типы и память Go golang собеседование
Map — это словарь: ключ → значение. Внутри хэш-таблица, не дерево. Поиск по ключу в среднем занимает примерно одно и то же время, сколько бы ключей ни было. Порядка ключей нет: обход каждый раз может идти иначе.
В бэкенде map нужна постоянно: индекс по id в памяти, набор флагов, дедупликация, кэш на процесс. Тип пишут так: map[K]V.
Аналогия: шкаф с ящиками. Хэш ключа говорит, в какой ящик смотреть. Два разных ключа могут попасть в один ящик — тогда внутри ящика ищут дальше. Это коллизия, не ошибка.
Comma-ok и нулевая map
Нулевое значение map — nil. Чтение из nil даёт нуль типа, как будто ключа нет. Запись в nil паникует: некуда класть ящики. Поэтому map создают через make или литерал, а не «объявили var и начали писать».
Индексация m[k] всегда возвращает значение. Если ключа нет, это нуль типа: 0, false, пустая строка, nil. Без второго результата нельзя понять: в map лежит настоящий ноль или ключа не было. Отсюда идиома comma-ok: v, ok := m[k].
func count(m map[string]int, k string) int {
v, ok := m[k]
if !ok {
return 0 // ключа нет; v тоже 0, но ok это показывает
}
return v
}
func mustStore() {
var m map[string]int // nil
// m["a"] = 1 // panic: assignment to entry in nil map
m = make(map[string]int)
m["a"] = 1
}
ok отличает отсутствие ключа от нулевого значения. Запись в nil map паникует.
Пример. Счётчик ошибок: ключа timeout нет. m["timeout"] вернёт 0. Без ok не отличить «ноль таймаутов» от «ключ ещё не заводили».
delete(m, k) на отсутствующем ключе безопасен. len(nil map) равен нулю. Цикл for k, v := range m специально идёт в случайном порядке. Даже на двух ключах порядок может меняться от запуска к запуску. Нужен стабильный вывод — снимите ключи и отсортируйте сами.
Рост и обход
Когда таблица заполняется, она расширяется: ящики копируются. На огромной map в горячем процессе это редкий, но видимый пик нагрузки. Запрос «все ключи от A до Z» map не умеет — только полный перебор. Для порядка и диапазона нужны дерево, отсортированный слайс или база.
Пример. Нужны пользователи с id от 100 до 200. Map по id так не спросишь. Либо перебрать все ключи, либо хранить их ещё и в слайсе / в БД.
Конкурентность
Две горутины меняют одну map без блокировки. Это data race. Рантайм часто роняет процесс: fatal error: concurrent map writes. Неважно, что ключи разные: общая сама таблица. delete, len и range — тоже доступы. Их защищают тем же мьютексом.
Пример. Два хендлера одновременно делают cache[id] = user. Ключи разные, map одна. Без лока процесс может упасть, а не «просто потерять одно значение».
type counters struct {
mu sync.Mutex
n map[string]int
}
func (c *counters) Inc(k string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.n == nil {
c.n = make(map[string]int)
}
c.n[k]++
}
Обычная map и мьютекс. Без Lock это data race, даже если ключи разные.
Много читателей и редкая запись — sync.RWMutex или шарды по ключу. sync.Map заточен под ключи, которые почти не меняются после записи, и под сценарий «пишут редко, читают часто». Это не замена обычной map в каждом сервисе. Для счётчика запросов часто хватает mutex вокруг обычной map.
Типичная ошибка
Считать map потокобезопасной «потому что ключи не пересекаются». Или защитить только запись, а range оставить без лока. Гонка на map — не потерянный инкремент, а потенциальный краш процесса. Вторая ловушка: писать в nil map после var m map[K]V.
На собеседовании
Map не потокобезопасна. Чтение без ключа даёт нуль типа — отличайте через comma-ok. Запись в nil паникует. Range идёт в случайном порядке. Delete, len и обход — тоже доступы к таблице. sync.Map — узкий случай, не замена всем map.