В прошлом посте мы придумали стек для локальных переменных функций. Всё работает отлично, но что делать, если переменная должна жить дольше функции?
————
Допустим, функция создаёт объект и возвращает адрес на него:
func CreateUser() *User {
// начинается с 0x1008
user := User{name: "Bob"} // 0x1008
// указатель: 0x1008 → 0x1028
return &user // возвращаем адрес 0x1008
// функция завершилась → указатель: 0x1028 → 0x1008
}
func main() {
// начинаем с 0x1000
var u *User // 0x1000
// указатель: 0x1000 → 0x1008
u = CreateUser() // получили адрес 0x1008
// CreateUser завершилась → указатель вернулся: 0x1028 → 0x1008
DoSomething() // начинается с 0x1008
fmt.Println(u.name) // 💩 читаем из 0x1008, но там уже другие данные
}Что произошло?
CreateUser() создала user в своём стековом фрейме и вернула на него адрес. Но когда функция завершилась, её стековый фрейм освободился — переменная user и её данные удалены.Теперь
u в main() указывает на невалидную память. Что там лежит? Никто не знает!Очевидно, стек не подходит для данных, которые должны пережить функцию. Что делать?
————
Попытка 1: Глобальная память
Может просто выделить отдельную область для таких данных?
var globalUsers [1000]User // зарезервировали 1000 мест
var nextIndex = 0
func CreateUser() *User {
user := &globalUsers[nextIndex]
nextIndex++
user.name = "Bob"
return user // безопасно - не в стеке
}
Это работает, но есть проблема: размер фиксирован. Нужно заранее угадать сколько объектов понадобится. 1000? 10000? А если миллион? А если в одной программе нужно 10, а в другой — миллион?
Плюс мы никогда ничего не освобождаем — даже если объект больше не нужен, его слот занят навсегда.
Нужно что-то более гибкое.
————
Решение: отдельная управляемая область
Да, вот такой резкий переход. Ну а как вы хотели? Это Telegram-пост, мнг ткста не влзт 😩
Давайте выделим большую область памяти отдельно от стека, где сможем:
- Выделять память когда нужно, динамически
- Любого размера
- Освобождать когда больше не нужно
Назовём эту область "куча" (heap):
// Стек: 0x1000..0x2000
// Куча: 0x10000..0x100000 (отдельная большая область)
func CreateUser() *User {
// Выделяем в куче
addr := allocate(32) // получили адрес 0x10100
user := (*User)(addr)
user.name = "Bob"
return user // 0x10100 - безопасно, не в стеке
}
func main() {
u := CreateUser() // 0x10100
DoSomething() // не затрёт - user в куче, не в стеке!
fmt.Println(u.name) // работает!
free(u) // освободили когда не нужно
}
Что ж, так гораздо лучше — данные живут независимо от вызовов функций.
————
Почему именно "куча"?
Важно: не путайте heap-память с heap-структурой данных (бинарной кучей для priority queue) - это совершенно разные вещи с одинаковым названием!
Название heap-память получила в противопоставление stack:
- Stack (стопка) — берём только сверху, строгий LIFO-порядок
- Heap (куча) — берём откуда угодно, любой порядок
В стеке память строго упорядочена. В куче же память выделяется и освобождается в любом порядке.
————
Цена гибкости
Куча решает проблему, но создаёт новые:
1. Медленно:
allocate() ищет свободное место, управляет списками блоков2. Фрагментация: память становится "дырявой" - много мелких свободных кусков вместо больших
3. Утечки памяти: забыл
free() → ячейка занята навсегда → программа жрёт всё больше памяти.Последнюю проблему решает сборщик мусора (GC). Вот о нём мы и поговорим в моём будущем ролике (подпишитесь, чтобы не пропустить 👍).
————
Итого: Стек vs Куча
Стек:
- Локальные переменные
- Автоматическое управление
- LIFO, очень быстро
- Ограничен по размеру
Куча:
- Динамические данные
- Ручное управление или GC
- Произвольный порядок, медленнее
- Большая
Теперь вы понимаете, почему в Go переменные иногда "убегают" (escape) в кучу — компилятор видит, что переменная используется после завершения функции, и автоматически выделяет её в куче вместо стека.
#guide #memory