В каких случаях в Go память для переменных выделяется на стеке, а не в куче? Что такое escape analysis?

Ответ

В Go решение о выделении памяти на стеке или в куче принимает компилятор во время компиляции. Этот процесс называется анализ побега (escape analysis).

Основное правило: если компилятор может доказать, что переменная не будет использоваться после завершения функции, в которой она была объявлена, он разместит её на стеке. В противном случае переменная «сбегает» (escapes) в кучу, чтобы её жизненный цикл не был ограничен стековым фреймом функции.

Когда память выделяется на стеке (не «сбегает»):

  1. Локальные переменные примитивных типов (int, float64, bool, string и т.д.), если на них не берут указатель, который затем возвращается из функции.
  2. Массивы фиксированного размера и структуры, которые являются value-типами, если они не содержат ссылок на данные в куче и не «сбегают» сами.
  3. Указатели, которые не выходят за пределы функции.

Когда память выделяется в куче («сбегает»):

  1. Возврат указателя на локальную переменную. Это классический пример. Переменная должна пережить вызов функции, поэтому её размещают в куче.
  2. Передача значения в интерфейс. Когда мы передаем значение (например, структуру) в функцию, принимающую interface{}, это значение обычно копируется в кучу, а указатель на эту копию сохраняется в значении интерфейса.
  3. Переменная используется в замыкании (closure), которое переживает функцию.
  4. Размер переменной неизвестен на этапе компиляции. Например, при создании среза make([]int, n), где n — переменная, базовый массив будет создан в куче.
  5. Срезы, мапы, каналы. Сами по себе эти типы являются небольшими структурами-заголовками (и могут быть на стеке), но они всегда ссылаются на базовые данные, которые располагаются в куче.

Пример:

// x будет на стеке
func stackAlloc() {
    x := 42
    _ = x 
}

// y "сбежит" в кучу, так как мы возвращаем на неё указатель
func heapAlloc() *int {
    y := 100
    return &y
}

Как проверить?
Узнать, куда компилятор разместил переменную, можно с помощью флага -gcflags="-m" при сборке или запуске:
go run -gcflags="-m" main.go