Память в Go — где живут данные решает компилятор
В Go нет new и delete в смысле C++ — есть сборщик мусора, и это создаёт иллюзию, что размещением данных думать не нужно. Иллюзия опасная: где именно живёт значение — в стеке кадра или в куче под управлением GC — решает компилятор на этапе компиляции, и от этого решения напрямую зависит, сколько работы достанется сборщику. Лишний escape в кучу не сломает программу, но превратит дешёвую локальную переменную в нагрузку на GC.
Ловушки этой темы группируются вокруг трёх заблуждений. Первое — что место размещения определяет ключевое слово (var, new, make): на самом деле его выбирает escape analysis по тому, как используется значение. Второе — что slice передаётся «по ссылке»: на самом деле копируется его трёхсловный заголовок, и две копии начинают разделять один backing array, отсюда баги с alias. Третье — что map можно адресовать как обычный массив: &m[k] не компилируется, и причина — в инкрементальной эвакуации бакетов при росте. Эта тема разбирает память Go по слоям — от устройства стека горутины до перехеширования map.
Карта темы
- Стек против кучи — у каждой goroutine свой небольшой растущий стек, куча под управлением GC, и где жить значению решает компилятор.
- Массивы против срезов — array это значение фиксированного размера, копируемое целиком, slice это заголовок над backing array.
- Заголовок среза — три машинных слова
{pointer, len, cap}и почему копия заголовка делит элементы с оригиналом. - Escape analysis — компиляторный проход, определяющий, переживает ли значение свой кадр, и потому уходит ли оно в кучу.
- Принудительное размещение в куче — случаи помимо обычного escape: неизвестный размер, слишком крупное значение, конверсия в interface.
- Рост среза — почему
appendсверхcapвыделяет новый backing array, и каков коэффициент роста. - Внутреннее устройство map — runtime-структура
hmap, массив бакетов по 8 слотов,tophashи overflow-бакеты. - Адресуемость элементов map — почему
&m[k]не компилируется и как эвакуация бакетов перемещает записи.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что место (стек/куча) определяет ключевое слово, а не escape analysis | Неверная ментальная модель аллокаций — не объяснить профиль -gcflags=-m |
| Думать, что стек goroutine фиксированного размера | Не понять, почему горутина дёшева и почему глубокая рекурсия не падает мгновенно |
| Полагать, что указательные типы всегда живут в куче | Промах с местом размещения — указатель на локальную переменную часто остаётся в стеке |
| Считать, что slice передаётся по ссылке | Не объяснить, почему append внутри функции иногда виден снаружи, а иногда нет |
| Думать, что копия slice независима от оригинала | Баг с alias — запись через одну копию портит элементы другой |
Путать len и cap | Неверное представление о том, когда append реаллоцирует backing array |
Думать, что append расширяет backing array на месте | Не объяснить, почему после реаллокации старые срезы не видят новых элементов |
Считать, что append всегда удваивает ёмкость | Неверная оценка памяти — для больших срезов коэффициент меньше |
| Называть escape analysis runtime-механизмом | Промах с пониманием — это статический анализ времени компиляции |
Думать, что make с переменной длиной может остаться в стеке | Неверная оценка аллокаций — неизвестный размер принудительно уходит в кучу |
| Считать map деревом или плоским массивом | Не объяснить overflow-бакеты, рандомизацию итерации и поведение при коллизиях |
Брать адрес элемента map через &m[k] | Код не компилируется — элемент map не адресуем |
| Думать, что эвакуация бакетов происходит вся сразу | Неверная модель стоимости вставки — эвакуация размазана по последующим записям |
Значение для собеседований
Память — обязательная тема на любом серьёзном Go-интервью. Спрашивают не «знаешь ли ты слово куча», а есть ли у вас рабочая модель того, что решает компилятор, а что runtime, и где это бьёт по производительности.
Что обычно проверяют:
- Разница между стеком и кучей — кто и когда освобождает каждую область, кто выбирает место.
- Что такое escape analysis — статический проход компилятора, а не runtime-наблюдение.
- Почему array копируется целиком, а slice — только заголовком, и к каким багам с alias это ведёт.
- Из каких трёх полей состоит slice header и как они меняются при reslice.
- Как растёт slice при
appendсверхcapи почему результат нужно присваивать обратно. - Внутреннее устройство map — бакеты,
tophash, overflow-бакеты, рост и перехеширование. - Почему
&m[k]не компилируется и что такое инкрементальная эвакуация бакетов.
Типичный неверный ответ: «слайс — это ссылочный тип, передаётся по ссылке». Это запускает разбор того, что копируется именно трёхсловный заголовок по значению — поэтому append, реаллоцировавший backing array, не виден снаружи функции, а append в пределах cap виден через общий массив.