Где в Go хранятся переменные: в стеке или в куче? От чего это зависит?

Ответ

В Go, в отличие от C/C++, программист не может явно указать, где разместить переменную — в стеке или в куче. Это решение принимает компилятор на этапе сборки программы.

Процесс, с помощью которого компилятор определяет место хранения, называется анализ побега (escape analysis).

Стек (Stack)

  • Что это: Область памяти для каждой горутины. Память выделяется и освобождается очень быстро.
  • Когда используется: Для локальных переменных функции (числа, структуры, указатели), если компилятор доказывает, что на эту переменную не будет ссылок после завершения функции. Память под них автоматически освобождается при выходе из функции.

Куча (Heap)

  • Что это: Общая область памяти для всей программы. Выделение памяти в куче медленнее, чем в стеке. Память управляется сборщиком мусора (Garbage Collector).
  • Когда используется: Когда переменная "сбегает" (escapes) из своей локальной области видимости. Это происходит, если на переменную будут ссылаться после того, как создавшая её функция завершится.

Причины "побега" переменной в кучу:

  1. Возврат указателя на локальную переменную. Это самый классический пример.
  2. Сохранение указателя в глобальной переменной или в структуре, которая сама находится в куче.
  3. Отправка указателя в канал.
  4. Размер переменной слишком велик для стека (хотя стек в Go может расти).

Пример:

// go build -gcflags='-m' ./main.go
// Флаг -m показывает результаты escape-анализа

package main

// bar возвращает int. Переменная 'y' не выходит за пределы функции.
// Компилятор разместит 'y' в стеке.
// ./main.go:10:6: can inline bar
func bar() int {
    y := 10
    return y
}

// foo возвращает *int. Указатель на 'x' будет использоваться после
// завершения foo, поэтому 'x' "сбегает" в кучу.
// ./main.go:16:6: can inline foo
// ./main.go:17:2: moved to heap: x
func foo() *int {
    x := 42
    return &x
}

func main() {
    b := bar()
    f := foo()
    println(b, *f)
}

Ключевая идея: Размещение переменных — это деталь реализации и оптимизация компилятора, направленная на повышение производительности. Разработчику не нужно об этом беспокоиться в большинстве случаев.