Память в Go
В Go нет new и delete в смысле C++ — есть сборщик мусора, и это создаёт иллюзию, что размещением данных думать не нужно. Иллюзия опасная: где именно живёт значение — в стеке кадра или в куче под управлением GC — решает компилятор на этапе компиляции, и от этого решения напрямую зависит, сколько работы достанется сборщику. Лишний escape в кучу не сломает программу, но превратит дешёвую локальную переменную в нагрузку на GC.
Ловушки этой темы группируются вокруг одного заблуждения — что место размещения определяет ключевое слово (var, new, make). На самом деле его выбирает escape-анализ по тому, как используется значение: указатель на локальную переменную часто остаётся в стеке, а обычная var-переменная уходит в кучу, если её адрес убегает. Поверх этого есть случаи, когда значение попадает в кучу помимо обычного escape — неизвестный размер, слишком крупное значение, конверсия в interface. А итог раскладки виден на скорости: непрерывный slice обгоняет связный список не из-за асимптотики, а из-за локальности кэша. Эта тема разбирает память Go по слоям — от устройства стека горутины до аппаратного prefetch.
Карта темы
- Стек против кучи — у каждой goroutine свой небольшой растущий стек, куча под управлением GC, и где жить значению решает компилятор, а не ключевое слово.
- Escape analysis — статический проход компилятора, определяющий, переживает ли значение свой кадр, и потому уходит ли оно в кучу.
- Принудительное размещение в куче — случаи помимо обычного escape: неизвестный размер, слишком крупное значение, конверсия в interface.
- new и make —
new(T)даёт обнулённый*Tдля любого типа,makeинициализирует slice, map или channel и возвращает готовое значение, а не указатель. - Локальность кэша — непрерывный slice обгоняет связный список на последовательном обходе за счёт префетча и редких промахов кэша.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что место (стек/куча) определяет ключевое слово, а не escape-анализ | Неверная ментальная модель аллокаций — не объяснить профиль -gcflags=-m |
| Думать, что стек goroutine фиксированного размера | Не понять, почему горутина дёшева и почему глубокая рекурсия не падает мгновенно |
| Полагать, что указательные типы всегда живут в куче | Промах с местом размещения — указатель на локальную переменную часто остаётся в стеке |
| Называть escape-анализ runtime-механизмом | Промах с пониманием — это статический анализ времени компиляции |
| Думать, что взятие адреса локальной переменной всегда вызывает escape | Escape наступает, только если адрес переживает кадр, — иначе значение в стеке |
| Думать, что escape-анализ — единственное, что кладёт значение в кучу | Неизвестный размер, слишком крупное значение и конверсия в interface уходят в кучу принудительно |
Думать, что make с переменной длиной может остаться в стеке | Неверная оценка аллокаций — неизвестный размер принудительно уходит в кучу |
Считать, что make возвращает указатель | Указатель отдаёт только new; make возвращает само значение slice/map/chan |
Применять new к map или каналу | new(map[K]V) даёт *map над nil-картой — запись по ней паникует, для рабочей карты нужен make |
Считать, что одинаковый O(n) обещает одинаковую скорость | На последовательном обходе slice обгоняет список из-за локальности и prefetch, а не асимптотики |
Хранить горячие данные как []*T ради «дешевизны» | Лишняя косвенность разбрасывает объекты по куче и убивает аппаратный prefetch |
Значение для собеседований
Память — обязательная тема на любом серьёзном Go-интервью. Спрашивают не «знаешь ли ты слово куча», а есть ли у вас рабочая модель того, что решает компилятор, а что runtime, и где это бьёт по производительности.
Что обычно проверяют:
- Разница между стеком и кучей — кто и когда освобождает каждую область, кто выбирает место.
- Что такое escape-анализ — статический проход компилятора, а не runtime-наблюдение.
- Почему тип значения сам по себе не решает место — указатель на локальную переменную часто остаётся в стеке.
- Какие случаи отправляют значение в кучу помимо обычного escape — неизвестный размер, чрезмерный размер, конверсия в interface.
- Чем
newотличается отmake— обнулённый указатель против готовой структуры, и почему оба не управляют размещением. - Почему непрерывный
sliceобгоняет связный список на последовательном обходе — кэш-линии и аппаратный prefetch.
Типичный неверный ответ: «указательные типы всегда живут в куче». Это запускает разбор того, что решает не тип, а escape-анализ: указатель на локальную переменную, не покидающую кадр, остаётся в стеке, а место размещения видно флагом -gcflags=-m.