Ответ
В Go решение о выделении памяти на стеке или в куче принимает компилятор во время компиляции. Этот процесс называется анализ побега (escape analysis).
Основное правило: если компилятор может доказать, что переменная не будет использоваться после завершения функции, в которой она была объявлена, он разместит её на стеке. В противном случае переменная «сбегает» (escapes) в кучу, чтобы её жизненный цикл не был ограничен стековым фреймом функции.
Когда память выделяется на стеке (не «сбегает»):
- Локальные переменные примитивных типов (
int
,float64
,bool
,string
и т.д.), если на них не берут указатель, который затем возвращается из функции. - Массивы фиксированного размера и структуры, которые являются value-типами, если они не содержат ссылок на данные в куче и не «сбегают» сами.
- Указатели, которые не выходят за пределы функции.
Когда память выделяется в куче («сбегает»):
- Возврат указателя на локальную переменную. Это классический пример. Переменная должна пережить вызов функции, поэтому её размещают в куче.
- Передача значения в интерфейс. Когда мы передаем значение (например, структуру) в функцию, принимающую
interface{}
, это значение обычно копируется в кучу, а указатель на эту копию сохраняется в значении интерфейса. - Переменная используется в замыкании (closure), которое переживает функцию.
- Размер переменной неизвестен на этапе компиляции. Например, при создании среза
make([]int, n)
, гдеn
— переменная, базовый массив будет создан в куче. - Срезы, мапы, каналы. Сами по себе эти типы являются небольшими структурами-заголовками (и могут быть на стеке), но они всегда ссылаются на базовые данные, которые располагаются в куче.
Пример:
// x будет на стеке
func stackAlloc() {
x := 42
_ = x
}
// y "сбежит" в кучу, так как мы возвращаем на неё указатель
func heapAlloc() *int {
y := 100
return &y
}
Как проверить?
Узнать, куда компилятор разместил переменную, можно с помощью флага-gcflags="-m"
при сборке или запуске:
go run -gcflags="-m" main.go