Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Runtime и память · Уровень: Senior+ ## TL;DR Garbage collector освобождает только то, на что **нет живых ссылок** — поэтому утечка в Go — это всегда «случайно удерживаемая ссылка» или незакрытый ресурс, а не ошибка аллокатора. Классика: растущие глобальные мапы, под-слайсы, держащие весь backing array, незакрытые `response.Body`/файлы, `time.Ticker`/`time.After` в циклах, кэши без эвикции, горутины-зомби. Ищут через `pprof` heap (`inuse_space` vs `alloc_space`), diff профилей и `runtime.MemStats`. ## Теория ### Почему GC не спасает Go GC — tracing mark-and-sweep: живо то, что достижимо от корней (стеки горутин, глобальные переменные, регистры). Объект собирается, только когда на него **нет ни одной ссылки**. «Утечка» — это логически ненужный объект, который остаётся достижимым: его кто-то держит. Дополнительно есть феномен «память освобождена для рантайма, но не возвращена ОС» (RSS остаётся высоким) — это не утечка в строгом смысле, но выглядит похоже. ```go // runtime.MemStats: ключевые поля var m runtime.MemStats runtime.ReadMemStats(&m) // HeapAlloc — байты живых объектов (примерно inuse) // HeapInuse — байты в используемых spans // HeapIdle — свободные spans (могут быть возвращены ОС) // HeapReleased — возвращено ОС // NextGC — порог следующего GC // Mallocs/Frees — кумулятивно; растущая разница Mallocs-Frees = рост живых объектов ``` ### Глобальные мапы: рост без удаления Самая частая утечка — мапа уровня пакета/синглтона, в которую только пишут. ```go var cache = map[string][]byte{} // живёт всё время работы процесса func remember(k string, v []byte) { cache[k] = v // никто никогда не удаляет -> неограниченный рост } ``` **Map не отдаёт память после `delete`.** Это критично для senior: ```go m := make(map[int]int) for i := 0; i < 1_000_000; i++ { m[i] = i } for i := 0; i < 1_000_000; i++ { delete(m, i) } // len(m) == 0, НО backing-бакеты по-прежнему выделены под 1M элементов! ``` Причина: `delete` помечает слот пустым, но рантайм **не уменьшает число бакетов** (нет shrink). Память бакетов вернётся, только если мапу полностью пересоздать: ```go m = make(map[int]int) // старая мапа целиком станет мусором и соберётся ``` Дополнительный нюанс: если значения мапы — указатели/структуры с указателями, удержание бакетов держит и их (хотя сами значения после delete недостижимы и соберутся). Но если значение — `[]byte` большого размера, важно реально `delete`, чтобы значение освободилось. ### Незакрытые ресурсы **`http.Response.Body`** — самая частая утечка в backend-коде. Незакрытое тело держит соединение (нет переиспользования из пула, утечка goroutine/fd) и буферы. ```go // Плохо resp, err := http.Get(url) if err != nil { return err } data, _ := io.ReadAll(resp.Body) // забыли Close -> connection не возвращается в пул, fd/goroutine утекают // Хорошо resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Чтобы соединение реально переиспользовалось — нужно ДОЧИТАТЬ тело: io.Copy(io.Discard, resp.Body) ``` Важно: `Close()` без полного чтения тела может не вернуть keep-alive соединение в пул. Идиома — `io.Copy(io.Discard, resp.Body)` перед `Close()`. **Файлы, соединения, prepared statements, rows:** ```go f, err := os.Open(path) if err != nil { return err } defer f.Close() // иначе утечка file descriptor (ulimit -> "too many open files") rows, err := db.Query(q) if err != nil { return err } defer rows.Close() // *sql.Rows держит соединение из пула до Close/полного прохода ``` ### Слайс-капканы **Под-слайс держит весь backing array.** Срез — это (ptr, len, cap) на общий массив. Маленький под-слайс не даёт собрать большой backing. ```go func leak() []byte { huge := make([]byte, 10<<20) // 10 MB return huge[:10] // вернули 10 байт, но держим 10 MB! } // Решение — скопировать нужную часть в свой массив: func noLeak() []byte { huge := make([]byte, 10<<20) out := make([]byte, 10) copy(out, huge[:10]) // huge собирается, держим 10 байт return out // Go 1.21+: return slices.Clone(huge[:10]) } ``` Тот же эффект при парсинге: если из большого буфера вырезать `[]byte`/строку под-слайсом и сохранить надолго — держится весь буфер. **Re-slice не освобождает, и truncate не зануляет элементы-указатели.** При укорачивании слайса со срезанным `len` элементы за новым `len` остаются в backing array и **не собираются GC**, если это указатели. ```go type Conn struct{ /* большой объект */ } func pop(s []*Conn) []*Conn { n := len(s) - 1 // Плохо: s[n] всё ещё указывает на *Conn в backing -> не собрётся return s[:n] } func popFixed(s []*Conn) []*Conn { n := len(s) - 1 s[n] = nil // обнуляем указатель в backing array -> *Conn соберётся return s[:n] } ``` Для удаления диапазона зануляйте «хвост»: ```go // Удаляем s[i:j], сдвигая остаток copy(s[i:], s[j:]) for k := len(s) - (j - i); k < len(s); k++ { s[k] = nil // обнуляем освободившиеся слоты } s = s[:len(s)-(j-i)] // Go 1.21+: slices.Delete делает это правильно (зануляет хвост) ``` Стандартная библиотека (`slices.Delete`, `slices.Compact` и т.д.) корректно зануляет освободившиеся элементы, если тип содержит указатели — используйте её. ### time.Ticker / time.Timer **`time.NewTicker` без `Stop`** утекает: тикер держит внутренний таймер в куче рантайма и продолжает слать в канал (до Go 1.23 даже не собирался GC, пока не остановлен). ```go // Плохо func worker() { t := time.NewTicker(time.Second) for range t.C { do() } // если из цикла выйти, t не остановлен } // Хорошо func worker(ctx context.Context) { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-t.C: do() case <-ctx.Done(): return } } } ``` **`time.After` в `for-select`** — классическая утечка: каждая итерация создаёт **новый** Timer, который живёт до своего срабатывания и не собирается раньше (до Go 1.23). При высокой частоте итераций — рост памяти. ```go // Плохо: новый таймер каждую итерацию for { select { case v := <-in: process(v) case <-time.After(time.Minute): // утечка таймеров при частом in return } } // Хорошо: переиспользуем один Timer с Reset t := time.NewTimer(time.Minute) defer t.Stop() for { select { case v := <-in: process(v) if !t.Stop() { select { case <-t.C: default: } // дренаж до Go 1.23 } t.Reset(time.Minute) case <-t.C: return } } ``` Замечание про Go 1.23+: таймеры стали обычными объектами кучи и собираются GC, даже если не остановлены; канал больше не буферизуется так, что нужен ручной дренаж. Но `Stop()`/корректное завершение горутин по-прежнему обязательны, и на старых версиях паттерн с `time.After` в цикле — реальная утечка. ### Кэши без эвикции Любая структура «положили и забыли» без ограничения размера/TTL — потенциальная утечка. Решения: - LRU/LFU с фиксированной ёмкостью (`hashicorp/golang-lru`). - TTL-эвикция фоновой горутиной или ленивая при чтении. - `sync.Map` сам по себе **не имеет эвикции** — это не кэш, а конкурентная мапа; те же проблемы роста. ```go // Утечка: sync.Map как «кэш» без вытеснения var cache sync.Map func put(k, v any) { cache.Store(k, v) } // растёт вечно ``` ### Ловушки с указателями в структурах - **Указатель на элемент большой структуры/слайса** держит весь объект. - **Замыкания**, захватывающие большой объект, держат его всё время жизни замыкания (например, переданного в долгоживущую горутину или таймер). - **`finalizer`** (`runtime.SetFinalizer`) может задерживать сборку на лишний цикл GC и создавать циклы удержания. - **Горутина-зомби**, заблокированная на канале/мьютексе навсегда, держит весь свой стек и всё, на что он ссылается, — частая «невидимая» утечка (виден рост `goroutine` профиля). ```go // Утечка горутины: отправитель блокируется навсегда, если читателя нет func leakGoroutine() { ch := make(chan int) // небуферизованный go func() { ch <- compute() // навсегда заблокирован, если никто не читает }() // забыли прочитать ch / нет ctx -> горутина и её стек живут вечно } ``` ## Как искать утечки ### pprof heap: inuse_space vs alloc_space ```go import _ "net/http/pprof" // регистрирует /debug/pprof/* go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() ``` ```bash # Снять heap-профиль go tool pprof http://localhost:6060/debug/pprof/heap # Внутри pprof: (pprof) top (pprof) list (pprof) web # граф (нужен graphviz) (pprof) png > heap.png ``` Четыре метрики heap-профиля: | Метрика | Что показывает | Когда использовать | |---------|----------------|--------------------| | `inuse_space` | байты **живых** объектов сейчас | поиск утечки (что удерживается) | | `inuse_objects` | число живых объектов | утечка по количеству мелких объектов | | `alloc_space` | всего аллоцировано за время жизни | поиск давления на GC (где аллоцируем) | | `alloc_objects` | всего объектов аллоцировано | частота аллокаций | ```bash go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap # утечки go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap # GC pressure ``` Ключ: **утечка ищется в `inuse_space`** — если он монотонно растёт между снимками, что-то удерживается. `alloc_space` высокий, но `inuse` стабильный — это не утечка, а просто много короткоживущих аллокаций (оптимизировать ради GC, но память не течёт). ### Diff профилей (главный приём) ```bash # Снять два снимка с интервалом под нагрузкой curl -s http://localhost:6060/debug/pprof/heap > heap1.out # ... подождать / прогнать нагрузку ... curl -s http://localhost:6060/debug/pprof/heap > heap2.out # Сравнить: что ВЫРОСЛО между снимками go tool pprof -inuse_space -base heap1.out heap2.out (pprof) top # покажет дельту по функциям — прямо к источнику утечки ``` `-base` (или `-diff_base`) показывает разницу — это самый надёжный способ найти, что именно накапливается со временем. Растущая строка в diff = кандидат на утечку. ### runtime.MemStats и goroutine-профиль ```go var m runtime.MemStats runtime.ReadMemStats(&m) log.Printf("HeapAlloc=%d MB, HeapObjects=%d, NumGoroutine=%d, NumGC=%d", m.HeapAlloc>>20, m.HeapObjects, runtime.NumGoroutine(), m.NumGC) ``` - Монотонно растущий `HeapAlloc`/`HeapObjects` при стабильной нагрузке = утечка. - Растущий `runtime.NumGoroutine()` = утечка горутин (часто первопричина утечки памяти). Профиль: `http://localhost:6060/debug/pprof/goroutine?debug=2`. - `HeapReleased` низкий при высоком `HeapIdle` — память не возвращена ОС (можно подтолкнуть `debug.FreeOSMemory()` / `GODEBUG=madvdontneed=1`, но это не лечит логическую утечку). ```bash # Профиль горутин — найти, где они залипли go tool pprof http://localhost:6060/debug/pprof/goroutine # или человекочитаемо со стеками: curl http://localhost:6060/debug/pprof/goroutine?debug=2 ``` ### Дополнительно - `GODEBUG=gctrace=1` — лог каждого GC (рост live heap между циклами заметен). - Continuous profiling (Pyroscope, Datadog, Grafana) для prod. - `testing` + `runtime.ReadMemStats` в тестах для регрессионных проверок аллокаций. ## Подводные камни / gotchas - `delete` из мапы не уменьшает число бакетов — память backing не возвращается; для реального освобождения нужно `m = make(...)`. - Под-слайс `huge[:10]` держит весь backing array `huge` — копируйте нужную часть (`slices.Clone`/`copy`). - `s = s[:n]` не зануляет элементы за `n`; если это указатели — они не соберутся. Используйте `slices.Delete` или ручное зануление хвоста. - `resp.Body` нужно и `Close()`, и (для keep-alive) дочитать (`io.Copy(io.Discard, body)`). - `time.After` в `for-select` создаёт таймер каждую итерацию — на Go <1.23 это утечка; используйте переиспользуемый `Timer` с `Reset`. - `time.NewTicker`/`NewTimer` без `Stop` — утечка таймеров (и горутин на старых версиях). - `sync.Map` — не кэш: нет эвикции, растёт неограниченно. - Заблокированная навсегда горутина держит весь стек и захваченные объекты; смотрите goroutine-профиль, а не только heap. - Высокий `alloc_space` ≠ утечка; утечка видна в растущем `inuse_space`/diff. - Высокий RSS при стабильном `HeapAlloc` — память не возвращена ОС, а не утечка (разные проблемы). ## Вопросы на собеседовании **В:** Если есть GC, как в Go вообще возможны утечки памяти? **О:** GC собирает только недостижимые объекты. Утечка — это логически ненужный объект, который остаётся достижимым: глобальная мапа без удаления, под-слайс, держащий большой backing, заблокированная горутина с её стеком, незакрытый ресурс, кэш без эвикции. Плюс отдельный феномен — память, освобождённая рантаймом, но не возвращённая ОС (высокий RSS), что не утечка, но похоже. **В:** Освобождается ли память мапы после `delete` всех ключей? **О:** Нет. `delete` помечает слоты пустыми, но рантайм не уменьшает число бакетов (нет shrink), поэтому backing остаётся выделенным под прежний размер. Сами значения после delete становятся недостижимыми и собираются, но структура бакетов — нет. Чтобы вернуть память, мапу нужно пересоздать: `m = make(map[K]V)`. **В:** Почему `return bigSlice[:10]` может привести к утечке? **О:** Срез — это (ptr, len, cap) на общий backing array. Под-слайс длиной 10 всё ещё указывает на весь массив, поэтому GC не может его собрать, пока жив под-слайс. Если нужна лишь малая часть надолго — скопировать в новый массив (`slices.Clone`/`copy`), тогда большой backing соберётся. **В:** Почему при `s = s[:n]` могут не собираться объекты, и как чинить? **О:** Усечение меняет только `len`; элементы между новым `len` и cap остаются в backing array. Если это указатели (или структуры с указателями), они достижимы и не собираются. Нужно занулить освободившиеся слоты (`s[n] = nil`) перед усечением. `slices.Delete`/`slices.Compact` делают это автоматически. **В:** Чем опасен `time.After` в `for-select` и как сделать правильно? **О:** `time.After` на каждой итерации создаёт новый `Timer`, который живёт до срабатывания. При частых итерациях накапливаются таймеры — утечка (особенно на Go <1.23, где они не собирались до срабатывания). Правильно — один `time.NewTimer` с `Stop()`+`Reset()` в цикле, либо контекст с одним таймаутом снаружи. **В:** Что нужно сделать с `http.Response.Body`, кроме `Close`? **О:** Для возврата keep-alive соединения в пул тело надо **дочитать**: `io.Copy(io.Discard, resp.Body)` перед `Close()`. Иначе соединение может не переиспользоваться (утечка соединений/fd/горутин транспорта), а не только память буферов. И `Close()` обязателен через `defer` сразу после проверки ошибки. **В:** В чём разница `inuse_space` и `alloc_space` в pprof и какой использовать для поиска утечки? **О:** `inuse_space` — байты живых объектов на момент снимка; `alloc_space` — всё, что аллоцировано за время жизни процесса (кумулятивно). Для утечки смотрят `inuse_space`: если он растёт между снимками — что-то удерживается. `alloc_space` показывает давление на GC (где много аллокаций), но высокий `alloc_space` при стабильном `inuse_space` — это не утечка. **В:** Как практически локализовать растущую утечку в проде? **О:** Снять два heap-снимка с интервалом под нагрузкой и сделать diff: `go tool pprof -inuse_space -base heap1 heap2`, затем `top`/`list` — растущие строки указывают на источник. Параллельно проверить `runtime.NumGoroutine()` и goroutine-профиль (`?debug=2`) на утечку горутин, и `MemStats` (`HeapAlloc`, `HeapObjects`) на тренд. Continuous profiling помогает увидеть тренд во времени. **В:** Как обнаружить утечку горутин и почему она ведёт к утечке памяти? **О:** Растущий `runtime.NumGoroutine()` при стабильной нагрузке и goroutine-профиль (`/debug/pprof/goroutine?debug=2`) со множеством горутин, залипших на одном стеке (`chan receive`, `select`, `sync.Mutex.Lock`). Каждая живая горутина держит свой стек и все объекты, на которые он ссылается (захваченные замыканием, переданные в канал и т.д.), поэтому утечка горутин почти всегда тянет за собой утечку памяти. Лечится `context`-отменой, таймаутами, корректным закрытием каналов. ## На что копают на senior+ - Чёткое понимание, что `delete` не shrink-ит мапу, и когда это критично (большие значения vs указатели в значениях). - Знание slice internals: под-слайс держит backing, усечение не зануляет хвост, full-slice expression `a[i:j:k]`, поведение `slices.Delete/Clone`. - Различие `inuse_*` и `alloc_*`, умение делать diff-профили (`-base`) и читать `list`/граф. - Связь утечки горутин с утечкой памяти; навык читать goroutine-профиль и находить точку блокировки. - Нюансы таймеров: поведение `time.After`/`Ticker` до и после Go 1.23, необходимость дренажа канала на старых версиях. - HTTP-пул соединений: почему важно дочитывать тело, как незакрытый Body утекает fd/горутины. - Различие «утечка» vs «RSS не возвращён ОС»: `HeapIdle`/`HeapReleased`, `debug.FreeOSMemory`, `GOMEMLIMIT`, madvise-стратегии. - Проектирование кэшей с эвикцией (LRU/TTL) и понимание, что `sync.Map` — не кэш. - Аккуратность с `runtime.SetFinalizer` (задержка сборки, циклы удержания) и с замыканиями, захватывающими большие графы объектов.