На каком этапе исполнения программы определяется, попадет ли переменная в стек или в кучу — на этапе компиляции или рантайма?
Что проверяют
Понимаете ли вы, что размещение переменной в Go решает компилятор, а не рантайм и не программист ключевым словом. Заодно смотрят, умеете ли вы это проверить инструментом, а не рассуждать по ощущениям.
Как отвечать
Решение принимается на этапе компиляции, механизм называется escape analysis. Компилятор смотрит, переживает ли значение вызов функции: если ссылка на него остаётся жива после выхода, значение «убегает» и размещается в куче, иначе остаётся на стеке. Проверяется это флагом компилятора:
go build -gcflags=-m ./...
# ./main.go:12:2: moved to heap: bufТипичные поводы для побега: возврат указателя на локальную переменную, сохранение указателя в структуру или глобальную переменную, захват переменной замыканием, которое живёт дольше функции, передача значения в интерфейс (например, аргументом в печать), размер, неизвестный на этапе компиляции.
Отдельно скажите, чего рантайм не делает: он не переносит переменные со стека в кучу по ходу работы и вообще не двигает объекты, сборщик мусора в Go не компактящий. Стек горутины при нехватке места растёт: рантайм выделяет больший стек и копирует содержимое, поправляя указатели, но это по-прежнему стек.
И поправка к расхожему правилу: «взяли адрес, значит куча» неверно. Указатель на локальную переменную, не покидающий функцию, спокойно живёт на стеке, тем более после встраивания вызова.
Где спотыкаются
Говорят, что срезы, карты и каналы всегда в куче, а числа всегда на стеке. Тип не решает ничего: небольшой не убежавший срез компилятор оставит на стеке, а обычный int уедет в кучу, если на него сохранили указатель в долгоживущей структуре.
Второе: приписывают решение рантайму, обычно фразой про переполнение стека, после которого объекты якобы переезжают в кучу. Стек просто копируется в больший, а размещение переменной к этому моменту давно зафиксировано компилятором.
Третье: оптимизируют вслепую, вычищая указатели ради сборщика мусора. Передача большой структуры по значению копирует её на каждом вызове, и выигрыш легко превращается в проигрыш. Смотреть надо вывод -gcflags=-m и бенчмарк с -benchmem.