Память в Go: Stack и Heap

Краткое описание

Разбираем, как Go работает с памятью: деление на стек и кучу, где хранятся переменные, как выделяется память для функций и как она освобождается.

Структура памяти

Выделение памяти приложению

При запуске программа запрашивает у операционной системы память. ОС выделяет блок памяти, который делится на две части:

  1. Stack (стек) — для простых типов и локальных переменных
  2. Heap (куча) — для сложных типов и динамических данных
┌─────────────────────────┐
│   Оперативная память    │
├─────────────────────────┤
│                         │
│    Stack (стек)         │
│    ┌──────────┐         │
│    │ main()   │         │
│    │ foo()    │         │
│    └──────────┘         │
│                         │
├─────────────────────────┤
│                         │
│    Heap (куча)          │
│    (больше размером)    │
│                         │
└─────────────────────────┘

Где хранятся переменные

Простые типы → Stack

Примитивные типы (int, bool, float64) хранятся на стеке.

var volume int = 123  // Хранится на стеке (64 бита в двоичном виде)

Стек:

┌──────────────┐
│ 123 (64 бит) │  ← volume
└──────────────┘

Сложные типы → Stack + Heap

Сложные типы (строки, слайсы, структуры) частично на стеке, частично на куче:

  • На стеке — ссылка/указатель
  • На куче — фактические данные
Stack           Heap
┌──────┐       ┌──────────────┐
│ ptr  │──────→│ данные       │
└──────┘       └──────────────┘

Стек для каждой функции

Важно: каждая функция имеет свой локальный стек (фрейм стека).

func main() {
    var volume int = 123  // На стеке main
    foo()
}

func foo() {
    var value int = 987   // На стеке foo
}

Визуализация:

Stack
┌────────────────┐
│  foo()         │
│  value = 987   │  ← Выделяется при вызове foo()
├────────────────┤
│  main()        │
│  volume = 123  │  ← Выделяется при запуске main()
└────────────────┘

Жизненный цикл

1. Запуск программы

ОС выделяет память → делится на Stack и Heap

2. Выполнение main()

func main() {
    var volume int = 123  // Выделяется на стеке main
    foo()
}

Stack:

┌─────────────┐
│ main()      │
│ volume=123  │
└─────────────┘

3. Вызов foo()

func foo() {
    var value int = 987  // Выделяется на стеке foo
}

Stack:

┌─────────────┐
│ foo()       │  ← Новый фрейм
│ value=987   │
├─────────────┤
│ main()      │
│ volume=123  │
└─────────────┘

4. Завершение foo()

Память foo освобождается, стек возвращается к main.

Stack:

┌─────────────┐
│ main()      │
│ volume=123  │
└─────────────┘

5. Завершение программы

Вся память возвращается ОС.

Ключевые моменты

1. Два типа памяти

  • Stack: быстрый, автоматическое управление, для простых типов
  • Heap: медленнее, требует сборки мусора, для сложных типов

2. Простые типы на стеке

int, int8, int16, int32, int64, uint, float32, float64, boolStack

3. Каждая функция имеет свой стек

При вызове функции выделяется фрейм стека, при завершении — освобождается.

4. Автоматическое управление

Go автоматически выделяет и освобождает память на стеке.

5. Куча для сложных данных

Строки, слайсы, карты, структуры → часть на стеке (ссылка), часть на куче (данные).

6. Сборщик мусора (GC)

За очистку кучи отвечает Garbage Collector (автоматически).

Что запомнить

  • Память делится на Stack и Heap
  • Простые типы (int, bool) → Stack
  • Каждая функция → свой фрейм стека
  • При завершении функции → память освобождается
  • Куча нужна для сложных типов
  • Go автоматически управляет памятью
  • Stack быстрее, Heap больше

Визуальная схема работы

1. Запуск программы
   ┌──────┐
   │ ОС   │ выделяет память
   └──────┘
   
2. Создание main()
   Stack: [main: volume=123]
   
3. Вызов foo()
   Stack: [foo: value=987]
          [main: volume=123]
          
4. Завершение foo()
   Stack: [main: volume=123]
   
5. Завершение программы
   Stack: []
   Память возвращается ОС

Итог: Go автоматически управляет памятью. Простые переменные на стеке, сложные — частично на куче. При завершении функции её стек освобождается. Всё просто и автоматически!