Слайсы, мапы и строки
Встроенные коллекции Go выглядят обманчиво просто: написал []int, map[string]int, string — и пользуйся. Но за каждой стоит конкретное устройство, и оно течёт наружу ровно там, где этого не ждёшь. slice — не массив и не указатель, а трёхсловный заголовок над отдельным backing array. map — не дерево и не плоский массив, а хеш-таблица на бакетах. string — не текст, а два неизменяемых слова. Пока вы не знаете эту раскладку, поведение остаётся набором необъяснимых сюрпризов.
И сюрпризы эти — стандартные вопросы на собеседовании. Slice передаётся «по значению», но две копии заголовка делят один массив — запись через одну видна через другую, отсюда баги с alias. append сверх cap выделяет новый массив, и старые срезы перестают видеть добавленное. Запись в nil map не «ничего не делает» — она паникует, хотя чтение из той же nil map безопасно. &m[k] не компилируется, и причина — в инкрементальной эвакуации бакетов при росте. А индексация среза с выходом за границу не портит соседнюю память, как в C, а паникует. Тему разбираем по слоям — от того, что такое slice, до перехеширования map и безопасности памяти.
Карта темы
- Массивы против срезов — array это значение фиксированного размера, копируемое целиком; slice это заголовок над общим backing array, и эта разница объясняет всё остальное.
- Заголовок среза — что такое slice и три машинных слова
{pointer, len, cap}; почему копия заголовка делит элементы с оригиналом. - Выражения среза —
s[low:high]и полноеs[low:high:max], как они задаютlenиcapнового среза. - Рост среза — почему
appendсверхcapвыделяет новый backing array, каков коэффициент роста, и чемnil-срез отличается от пустого. - Алиасинг срезов — два среза над одним backing array, классическая ловушка
appendи защита через трёхиндексное выражение. - Основы map —
make(map[K]V), проверкаv, ok := m[k], нулевое значение для отсутствующего ключа и намеренно рандомизированный порядок обхода. - Внутреннее устройство map — runtime-структура
hmap, бакеты по 8 слотов,tophash, overflow-бакеты, рост и почему&m[k]не компилируется. - Нулевая map — чтение
nilmap безопасно и даёт нулевое значение, а запись паникуетassignment to entry in nil map. - Строки —
stringэто неизменяемые байты UTF-8;lenсчитает байты,range— руны; заголовок{pointer, len}16 байт, а конвертация в[]byteкопирует. - Безопасность памяти — индексация среза проверяется по границам и паникует вместо порчи соседней памяти; как трёхиндексное выражение ограничивает
cap.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что slice передаётся «по ссылке» | Не объяснить, почему append внутри функции иногда виден снаружи, а иногда нет |
| Думать, что копия slice независима от оригинала | Баг с alias — запись через одну копию портит элементы другой |
Путать len и cap | Неверное представление о том, когда append реаллоцирует backing array |
Думать, что append расширяет backing array на месте | Не объяснить, почему после реаллокации старые срезы не видят новых элементов |
Не присваивать результат append обратно | Потеря нового заголовка — обновлённые len/pointer пропадают |
Путать nil-срез и пустой срез | Оба len 0, но nil равен nil и маршалится в null, а []int{} — в [] |
Забыть трёхиндексное s[low:high:max] при нарезке общего буфера | append в подрез тихо перезапишет данные соседнего среза |
| Считать map деревом или плоским массивом | Не объяснить overflow-бакеты, рандомизацию итерации и поведение при коллизиях |
| Полагаться на порядок обхода map | Порядок намеренно рандомизирован — для детерминизма соберите и отсортируйте ключи |
Брать адрес элемента map через &m[k] | Код не компилируется — элемент map не адресуем из-за эвакуации бакетов |
Думать, что запись в nil map «просто ничего не сделает» | На деле panic: assignment to entry in nil map в рантайме |
Путать len(s) строки с числом символов | Для многобайтовых рун это разные числа — len считает байты UTF-8 |
Считать string(b) и []byte(s) бесплатными | Каждая конвертация копирует все байты — это аллокация |
| Ждать C-подобного переполнения буфера при выходе за границу | Go проверяет границы и паникует index out of range, а не портит соседнюю память |
Значение для собеседований
Встроенные структуры данных — обязательная тема на любом Go-интервью. Спрашивают не синтаксис []T или map[K]V, а есть ли у вас рабочая модель того, что лежит под капотом и как это устройство течёт наружу в поведении.
Что обычно проверяют:
- Что такое slice, из каких трёх полей состоит заголовок и как они меняются при reslice.
- Почему array копируется целиком, а slice — только заголовком, и к каким багам с alias это ведёт.
- Как растёт slice при
appendсверхcap, почему результат нужно присваивать обратно и чемnil-срез отличается от пустого. - Устройство map — бакеты,
tophash, overflow-бакеты, рост, почему&m[k]не компилируется и почему порядок обхода рандомизирован. - Почему чтение из
nilmap безопасно, а запись паникует. - Из чего состоит заголовок строки, почему
lenсчитает байты, аrange— руны, и почему конвертация в[]byteкопирует. - Почему индексация среза с выходом за границу паникует, а не портит память.
Типичный неверный ответ: «слайс — это ссылочный тип, передаётся по ссылке». Это запускает разбор того, что копируется именно трёхсловный заголовок по значению — поэтому append, реаллоцировавший backing array, не виден снаружи функции, а append в пределах cap виден через общий массив.