Где в памяти находится переменная: в хипе или в стеке в Go
Что проверяют
Знаете ли вы, что в Go место под переменную выбирает не программист, а компилятор через escape analysis. Следом спрашивают, как это проверить и почему лишние аллокации в хипе плохи для производительности.
Как отвечать
Спецификация Go вообще не говорит о стеке и хипе. Компилятор смотрит, может ли значение пережить функцию. Если доказал, что нет, кладёт на стек, иначе значение «убегает» в хип, и за ним следит сборщик мусора.
Типичные причины побега:
- указатель наружу:
return &xв Go законен,xпросто переезжает в хип; - интерфейсы: значение, переданное как
any, часто уходит в хип, классика этоfmt.Println(x); - замыкания и горутины: захваченная переменная, которую замыкание переживает, уходит в хип;
- размер неизвестен при компиляции:
make([]byte, n)с переменнымnобычно в хипе, аmake([]byte, 64), не покидающий функцию, может остаться на стеке; - слишком большие значения: их компилятор сразу размещает в хипе.
Проверка: go build -gcflags=-m ./... печатает moved to heap: x, escapes to heap и does not escape, а -m=2 объясняет причину. Эффект измеряют через go test -bench . -benchmem (колонка allocs/op) и профиль памяти в pprof.
Про стеки горутин: они начинаются с нескольких килобайт и растут копированием в больший участок. Это возможно, потому что escape analysis гарантирует: указатели на стек не хранятся в хипе.
Где спотыкаются
Говорят «new и make выделяют в хипе, var на стеке». Решает только escape analysis: new(T) внутри функции может оказаться на стеке, а обычная var x уйдёт в хип, если её адрес вернули.
Повторяют, что стек в Go фиксированного размера, а хипом надо управлять вручную. Стек горутины растёт, а хип освобождает сборщик мусора.
Оптимизируют вслепую: переписывают указатели на значения без профиля. Копирование больших структур бывает дороже аллокации, а поведение анализа меняется от версии к версии компилятора, поэтому смотреть надо вывод -gcflags=-m.