Локальная переменная живёт на стеке горутины, пока компилятор не решит, что она «убегает». Тогда объект на куче, и его жизнь оплачивает сборщик мусора.
Рантайм и инструменты Go golang собеседование
У каждой горутины свой стек: быстро выделяется, растёт и сжимается. Куча процесса общая: объекты переживают функцию, их собирает GC. Куда попадёт переменная, решает анализ побега на этапе компиляции. В исходнике нет слова «положи на стек», как в C.
На бэкенде это про аллокации в горячем пути. Лишний указатель, интерфейс, замыкание в цикле — давление на сборщик, хвост задержки, больше CPU. Микрооптимизация вслепую вредна. Сначала бенчмарк и флаг компилятора, потом упрощение кода.
Аналогия: стек — блокнот на столе, живёт, пока вы за этим столом. Куча — общий шкаф офиса. Положили адрес блокнота в шкаф или отдали коллеге — блокнот тоже кладут в шкаф, иначе после ухода со стола адрес станет пустым.
Что заставляет escape
Вернули указатель на локальную переменную — она должна пережить функцию, значит куча. Сохранили адрес в структуре, которая уже на куче, в глобальной, в слайсе, который уходит наружу. Положили значение в интерфейс: часто упаковка, объект на куче. Точный случай зависит от компилятора.
func heapInt() *int {
x := 1
return &x // x escapes to heap
}
func stackOK() int {
x := 1
return x // значение, не адрес: x может остаться на стеке
}
// go build -gcflags='-m' .
// ищите строки «escapes to heap»
Указатель наружу — побег на кучу. Флаг -m показывает решение компилятора.
Пример. Хендлер собирает user := User{Name: name} и делает return &user. Структура уезжает на кучу. Если вернуть значение User, шанс остаться на стеке выше. Не цель сама по себе — смотрите бенчмарк.
Замыкание, которое уходит в горутину или в слайс колбэков, захватывает переменные — они живут с замыканием на куче. append в слайс, который возвращаете, кладёт элементы туда, где живёт массив: часто куча. Это нормально. Плохо, когда в горячем цикле плодят лишние мелкие объекты без нужды.
Как смотреть
go build -gcflags='-m' пишет, что ушло на кучу и почему. Читать всё подряд бессмысленно: смотрят функции из профиля. В бенчмарке b.ReportAllocs() и -benchmem показывают allocs/op и B/op. Падение аллокаций после упрощения типа — проверяемый эффект, не легенда.
Не боритесь с побегом ради борьбы. Читаемый код с одной аллокацией на запрос лучше нечитаемого с нулём. Имеет смысл горячий путь сериализации, копирование буферов, лишние fmt.Sprintf и интерфейсы на каждый мелкий тип. Стек не бесконечен: глубокая рекурсия и огромный массив в кадре могут раздуть стек горутины. Огромный массив в кадре часто как раз уезжает на кучу.
Типичная ошибка
Обещать «в Go всё на стеке» или наоборот «new всегда куча». Решает компилятор. Вернуть указатель на локальную переменную — типичный побег. Гнаться за нулём аллокаций без бенчмарка. Путать стек горутины с кучей процесса и с двоичной кучей как структурой данных.
На собеседовании
Указатель или интерфейс наружу → куча → сборщик. Смотреть -gcflags=-m и allocs в бенчмарке. В горячем пути убирают лишние упаковки и замыкания в цикле, а не переписывают весь сервис.