Где в Go аллоцируются переменные: в стеке или в куче?

Ответ

В Go, в отличие от C++, решение о размещении переменной в стеке (stack) или в куче (heap) принимает компилятор, а не программист с помощью ключевых слов. Этот процесс называется анализом побега (escape analysis).

Основное правило:

  • Стек (Stack): Память выделяется для переменных, время жизни которых ограничено вызовом одной функции. Это очень быстро, так как память освобождается автоматически при выходе из функции.
  • Куча (Heap): Память выделяется для переменных, которые должны "пережить" функцию, в которой были созданы. Такие переменные "сбегают" (escape) в кучу. Память в куче управляется сборщиком мусора (GC).

Ключевой механизм: Escape Analysis

Компилятор анализирует код, чтобы определить, может ли ссылка на переменную быть использована за пределами её исходной области видимости. Переменная "сбегает" в кучу, если:

  1. Возвращается указатель на переменную: Если функция возвращает указатель на свою локальную переменную, эта переменная должна быть размещена в куче, иначе указатель будет указывать на мусор.
    func getIntPtr() *int {
        i := 42
        return &i // "i" сбегает в кучу
    }
  2. Переменная слишком большая: Массивы или структуры очень большого размера могут быть принудительно размещены в куче, даже если они не "сбегают", чтобы не вызвать переполнение стека.
  3. Захват переменной в замыкании (closure): Если горутина или замыкание захватывает переменную из внешней функции, и время жизни горутины/замыкания превышает время жизни функции, переменная сбегает в кучу.
    func main() {
        x := 10
        go func() {
            fmt.Println(x) // "x" сбегает в кучу, т.к. используется в другой горутине
        }()
    }
  4. Отправка указателя в канал: Это частный случай, когда переменная становится доступной в другой части программы (другой горутине).

Пример: Стек vs Куча

// user останется в стеке, т.к. используется только внутри функции
func stackAlloc() {
    user := User{Name: "Alice", Age: 30}
    // ... работа с user
}

// user "сбежит" в кучу, т.к. на него возвращается указатель
func heapAlloc() *User {
    user := User{Name: "Bob", Age: 25}
    return &user
}

Как проверить? Вы можете попросить компилятор сообщить о результатах escape-анализа с помощью флага -m: go build -gcflags="-m" .