В каких случаях память точно будет выделяться в стеке?

Ответ

В Go память выделяется в стеке, когда компилятор с помощью анализа побега (escape analysis) может доказать, что объект не нужен за пределами текущей функции. Это означает, что время жизни объекта полностью ограничено функцией, в которой он был создан. Основные случаи, когда память вероятно будет выделена в стеке:

  1. Локальные переменные примитивных типов (int, float, bool, string и т.д.), если они не передаются по указателю и не возвращаются из функции.
    func foo() {
        x := 42 // x выделится в стеке
    }
  2. Локальные переменные структур и массивов фиксированного размера, если они не содержат указателей, которые "убегают" из функции, и сами структуры/массивы не возвращаются из функции.
    type Point struct {
        X, Y int
    }
    func bar() {
        p := Point{1, 2} // p выделится в стеке
        arr := [3]int{1, 2, 3} // arr выделится в стеке
    }
  3. Небольшие объекты, которые компилятор может оптимизировать и разместить на стеке, даже если они могли бы потенциально "убежать", но их использование позволяет это. (Это более продвинутый случай оптимизации компилятора).

Когда память вероятно будет выделена в куче (из-за "побега" объекта):

  • Возврат указателя на локальную переменную из функции:
    func createInt() *int {
        x := 10
        return &x // &x "убегает", x будет в куче
    }
  • Передача указателя на локальную переменную в другую горутину:
    func spawnWorker() {
        data := "hello"
        go func() {
            // data "убегает" в другую горутину, будет в куче
            println(data)
        }()
    }
  • Присвоение локальной переменной глобальной переменной или полю структуры, которая "убегает":
    var globalVar *int
    func assignGlobal() {
        localVal := 5
        globalVar = &localVal // &localVal "убегает", localVal будет в куче
    }
  • Использование интерфейсов: Если значение присваивается переменной интерфейсного типа, и это значение (или сама переменная интерфейса) "убегает" из функции, то базовое значение будет выделено в куче.

    type Greeter interface {
        Greet() string
    }
    type Person struct { Name string }
    func (p Person) Greet() string { return "Hello, " + p.Name }
    
    func createGreeter() Greeter {
        p := Person{Name: "Alice"}
        return p // p присваивается интерфейсу и возвращается, p будет в куче
    }
  • Создание срезов (slices) с динамическим размером или их расширение: Хотя сам заголовок среза (указатель, длина, емкость) может быть на стеке, базовый массив, на который он ссылается, обычно выделяется в куче, если его размер не известен на этапе компиляции или если он расширяется.
    s := make([]int, 10) // Базовый массив s будет в куче

Важно помнить, что анализ побега — это оптимизация компилятора, и его поведение может меняться между версиями Go. Для точного определения можно использовать флаг компилятора -gcflags='-m'.