Как происходит выделение памяти для переменных в функции double в Go
Что проверяют
Понимаете ли вы escape analysis: где окажется переменная, решает компилятор, а не способ её объявления. Следом почти всегда просят поменять double так, чтобы переменная ушла в кучу, и показать, как это проверить.
Как отвечать
Возьмите простой вариант:
func double(n int) int {
r := n * 2
return r
}n и r не покидают функцию, поэтому живут в кадре стека горутины, а скорее всего вообще в регистрах: с Go 1.17 на amd64 аргументы и результаты передаются через регистры. После возврата кадр просто переиспользуется, сборщику мусора работы нет. Такую маленькую функцию компилятор может и вовсе встроить в место вызова.
Теперь вариант с указателем:
func double(n int) *int {
r := n * 2
return &r
}В C это была бы ошибка, а в Go корректный код: компилятор видит, что адрес r переживёт функцию, и размещает r в куче. Проверяется флагом go build -gcflags=-m, в выводе будет moved to heap: r.
Что ещё уводит значение в кучу:
- интерфейсы: передача в
fmt.Printlnили вanyчасто заставляет значение «убежать»; - замыкания: переменная, захваченная горутиной или функцией, которая живёт дольше кадра;
- неизвестный размер:
make([]byte, n)сnиз рантайма или слишком большой объект.
И наоборот, new(int) или make не означают кучу: если результат не убегает, он останется на стеке.
Про сам стек: у горутины он стартует с нескольких килобайт и растёт по мере нужды, рантайм копирует его в больший участок и поправляет указатели. Фиксированного размера, как у потока в C, нет.
Где спотыкаются
Говорят «new и make выделяют в куче, остальное на стеке». Решает только анализ убегания.
Считают стек горутины фиксированным. Тогда непонятно, почему глубокая рекурсия в Go падает только на лимите около гигабайта на 64-битных системах.
Советуют передавать всё по указателю «для скорости». Указатель часто и отправляет значение в кучу, а мелкую структуру дешевле скопировать.