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