В языках вроде C/C++ вы сами решаете, где выделить память: на стеке (быстро, удалится само) или в куче (медленно, нужно чистить руками).
В Java почти всё летит в кучу.
А в Go вы пишете
x := 10 и понятия не имеете, где эта переменная будет жить. За вас это решает компилятор с помощью механизма, который называется Escape Analysis (Анализ утечек).Сеньоры знают: чем больше объектов "убегает" со стека в кучу, тем больше работы у Garbage Collector'а, и тем медленнее работает сервис.
Давайте посмотрим, как компилятор принимает это решение.
Стек vs Куча (Кратко)
• Стек (Stack): Память для локальных переменных функции. Выделяется мгновенно, очищается автоматически и бесплатно при выходе из функции (просто сдвигается указатель). Никакого GC!
• Куча (Heap): Общая память. Выделение стоит процессорного времени (нужно найти свободный кусок), а для очистки нужно запускать тяжелый Garbage Collector.
Как переменная "убегает" в кучу? (Три главных триггера)
Компилятор Go консервативен. Если он хоть на секунду сомневается, переживет ли переменная функцию, в которой была создана, он отправляет её в кучу (от греха подальше).
❌ 1. Возврат указателя наружу
func GetUser() *User {
u := User{ID: 1, Name: "Ivan"}
return &u // Утечка!
}
Переменная
u создана внутри функции. Но мы возвращаем указатель на неё. Если бы компилятор оставил её на стеке, то после выхода из GetUser стек бы очистился, и указатель стал бы указывать на мусор (dangling pointer). Компилятор это видит и заботливо переносит u в кучу.❌ 2. Динамический размер (Слайсы и Мапы)
func Generate(n int) {
// Утечка! Компилятор не знает размер 'n' на этапе компиляции,
// поэтому не может выделить место на стеке.
buf := make([]byte, n)
}
Как починить? Если размер буфера известен заранее (например, 64 байта), используйте массивы
[64]byte, а не слайсы. Массивы живут на стеке.❌ 3. Ловушка интерфейса
any (interface{})
func DoLog() {
x := 42 // Казалось бы, обычный int
fmt.Println(x) // Утечка!
}
Почему
x убежал в кучу? Потому что fmt.Println принимает аргументы типа any. Чтобы передать int в interface{}, Go должен "упаковать" значение (boxing). Интерфейсы динамические, и компилятор не может гарантировать безопасность ссылок, поэтому всё, что попадает в fmt.Println, летит в кучу.(Именно поэтому в бенчмарках никогда не используют логгеры - они искажают картину аллокаций).
🛠 Как проверить свой код?
Не нужно гадать. Тулчейн Go умеет показывать все утечки. Соберите код с флагом
-gcflags="-m":
go build -gcflags="-m" main.go
В терминале вы увидите строки:
./main.go:10:2: moved to heap: u./main.go:15:13: x escapes to heap🔥 Миф про производительность указателей
Многие разработчики везде передают структуры по указателю
func Process(u *User), думая: "Структура весит 100 байт, я лучше передам указатель (8 байт), чтобы не копировать память!"Это классическая ошибка преждевременной оптимизации.
Да, копирование 100 байт на стеке займет пару наносекунд. Но передав указатель, вы можете спровоцировать Escape Analysis выбросить эту структуру в кучу. В итоге вы сэкономите 2 наносекунды на копировании, но потратите микросекунды на выделение памяти в куче и добавите работы GC.
Золотое правило: Передавайте по значению всё, что не нужно изменять (mutate) внутри функции, если это не гигантская структура на мегабайт. Стек невероятно быстрый.
#golang #underhood #performance #memory #bestpractices
📲 Мы в MAX
👉 @golang_lib