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 Утечка горутины — это горутина, которая никогда не завершается и не освобождается рантаймом, потому что навсегда заблокирована на канале/мьютексе/системном вызове или крутится в бесконечном цикле без условия выхода. Каждая утёкшая горутина удерживает свой стек (минимум 2 КБ, растёт сегментами) плюс всё, что захвачено через замыкание, поэтому утечки проявляются как медленный рост памяти и `runtime.NumGoroutine()`. Главные инструменты диагностики — pprof goroutine profile и дамп по `SIGQUIT`/`debug=2`; главная профилактика — `context.Context`, `errgroup` и чёткий контракт «кто закрывает канал». ## Теория Горутина в Go — это не поток ОС, а легковесная сущность, которой управляет планировщик (scheduler) рантайма по модели M:N (G — горутина, M — поток ОС, P — логический процессор). Сборщик мусора **не собирает** заблокированные горутины: с точки зрения GC горутина, висящая на `chan recv`, — это живой объект, корень для своего стека. Нет механизма «таймаута» или «прерывания» горутины извне — горутина завершается только тогда, когда возвращается её функция. Отсюда фундаментальное следствие: **если вы запустили горутину, вы обязаны гарантировать путь к её завершению**. ### Что именно течёт Когда горутина «зависает», утекает: | Ресурс | Размер / детали | |---|---| | Стек горутины | Стартует с 2 КБ (`_StackMin`), растёт удвоением сегментами; для зависшей горутины может остаться большим, т.к. усадка стека (stack shrink) бывает только во время GC и при определённых условиях | | Захваченные переменные | Всё, на что ссылается замыкание горутины, остаётся живым: буферы, соединения, большие структуры | | Записи в планировщике | G-структура в пуле, не возвращается | | Внешние ресурсы | Открытые `net.Conn`, файловые дескрипторы, транзакции БД, если горутина владеет ими | Важно: `defer conn.Close()` в зависшей горутине **никогда не выполнится**, потому что функция не возвращается. Это значит, что утечка горутин часто тянет за собой утечку FD и соединений. ### Причина 1: send в unbuffered-канал без читателя ```go func leak1() { ch := make(chan int) // unbuffered go func() { ch <- 42 // блокируется навсегда, если никто не читает }() // функция вернулась, читателя нет → горутина утекла } ``` Классика: горутина пишет результат в канал, но вызывающий код уже ушёл (например, по таймауту контекста), и читать никто не будет. `send` на unbuffered-канале требует рандеву с `recv` — без него горутина паркуется навсегда (`gopark`, состояние `chan send`). ### Причина 2: receive без отправителя ```go func leak2() { ch := make(chan int) go func() { v := <-ch // никто никогда не пошлёт и не закроет канал fmt.Println(v) }() } ``` Симметричная проблема. Если канал нигде не закрывается и никто не пишет — горутина висит в состоянии `chan receive`. Особенно коварно при паттерне «слушаем результат», когда producer упал с паникой/ошибкой до отправки. ### Причина 3: отсутствие отмены через context Самый частый источник утечек в продакшене — долгоживущие горутины (пуллеры, воркеры, фоновые таски), запущенные без механизма остановки. ```go // ПЛОХО: горутину невозможно остановить func startWorker(jobs <-chan Job) { go func() { for j := range jobs { process(j) } }() } // ХОРОШО: отмена через context func startWorker(ctx context.Context, jobs <-chan Job) { go func() { for { select { case <-ctx.Done(): return // гарантированный выход case j, ok := <-jobs: if !ok { return } process(ctx, j) } } }() } ``` ### Причина 4: забытый WaitGroup ```go func leak4() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func() { // забыли defer wg.Done() doWork() }() } wg.Wait() // блокируется навсегда → теперь утекла ГОРУТИНА-ВЫЗЫВАТЕЛЬ } ``` Здесь два вида беды: либо `wg.Done()` не вызывается и `Wait()` висит вечно, либо `wg.Add` вызван внутри горутины (race с `Wait`). Правило: `wg.Add(n)` — до запуска горутин, `defer wg.Done()` — первой строкой в горутине. ### Причина 5: deadlock на mutex ```go func leak5() { var mu sync.Mutex mu.Lock() go func() { mu.Lock() // ждёт навсегда, владелец не отпустит defer mu.Unlock() // ... }() // забыли mu.Unlock() в основном потоке } ``` Если рантайм обнаруживает, что **все** горутины заблокированы, он паникует с `fatal error: all goroutines are asleep - deadlock!`. Но если хотя бы одна горутина работает (например, HTTP-сервер крутится), частичный deadlock на подмножестве горутин рантайм **не заметит** — это и есть утечка. Частая причина: рекурсивный захват не-рекурсивного мьютекса, либо несогласованный порядок блокировок (lock ordering) у нескольких мьютексов. ### Причина 6: бесконечный for без выхода ```go func leak6() { go func() { for { poll() // нет select на ctx.Done(), нет break-условия time.Sleep(time.Second) } }() } ``` Тикеры и поллеры без канала отмены живут до конца процесса. Отдельный подвид — `time.Tick` (в отличие от `time.NewTicker`) **нельзя остановить**, утекает сам таймер. ### Под капотом: как горутина «паркуется» Когда горутина блокируется на канале, планировщик вызывает `gopark`: горутина переводится в состояние `_Gwaiting`, её M освобождается под другие G. Состояние видно в дампе (`chan receive`, `chan send`, `select`, `semacquire` для мьютекса, `IO wait` для сети). Эти строки — главная подсказка при разборе утечки: они говорят, **на чём** и **где** (по стеку) висит горутина. ```bash # Состояния, которые вы увидите в дампе: # [chan receive] — recv без отправителя # [chan send] — send без читателя # [select] — select без готовых case и без default # [semacquire] — ждёт sync.Mutex / sync.WaitGroup # [IO wait] — сетевой read/write (netpoller) # [sync.Cond.Wait] — ждёт сигнала на условной переменной ``` ### Диагностика #### runtime.NumGoroutine — первый сигнал ```go import ( "expvar" "runtime" ) func init() { expvar.Publish("goroutines", expvar.Func(func() any { return runtime.NumGoroutine() })) } ``` Постоянно растущий `NumGoroutine` под стабильной нагрузкой = почти наверняка утечка. Выведите метрику в Prometheus (у `go_goroutines` это делает стандартный `client_golang`). #### pprof goroutine profile ```go import _ "net/http/pprof" // регистрирует /debug/pprof/* на DefaultServeMux func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // ... } ``` ```bash # Сводка по горутинам (агрегировано по стеку) go tool pprof http://localhost:6060/debug/pprof/goroutine # Полный текстовый дамп с дедупликацией curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=1' | less # Дамп БЕЗ дедупликации, полные стеки каждой горутины curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=2' | less # Граф/флеймграф в браузере go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine ``` `debug=1` группирует одинаковые стеки и показывает счётчик — идеально, чтобы увидеть «1500 горутин висят на одном и том же `chan receive` в строке X». `debug=2` показывает каждую горутину отдельно с её возрастом и состоянием. Сравнение двух профилей во времени (diff) точно покажет, какой стек растёт: ```bash curl -s 'http://localhost:6060/debug/pprof/goroutine' -o g1.pprof sleep 60 curl -s 'http://localhost:6060/debug/pprof/goroutine' -o g2.pprof go tool pprof -base g1.pprof g2.pprof # дельта между снимками ``` #### Дамп через SIGQUIT Если pprof не подключён, можно получить полный стек-дамп всех горутин и аварийно завершить процесс: ```bash kill -SIGQUIT # или Ctrl+\ в терминале # Эквивалентно установке GOTRACEBACK=all и панике ``` Управление детализацией дампа: ```bash GOTRACEBACK=none # минимум GOTRACEBACK=single # только текущая горутина (по умолчанию для паники) GOTRACEBACK=all # все пользовательские горутины GOTRACEBACK=system # + системные горутины рантайма GOTRACEBACK=crash # all + дамп ядра (core dump) ``` #### goleak от Uber в тестах `go.uber.org/goleak` ловит утечки на этапе тестирования — лучший способ не дать им доехать до прода. ```go import "go.uber.org/goleak" // Вариант 1: проверка всего пакета разом func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } // Вариант 2: точечно в конкретном тесте func TestWorker(t *testing.T) { defer goleak.VerifyNone(t) // ... тело теста; в конце goleak убедится, что не осталось лишних горутин } ``` goleak делает снимок горутин, отфильтровывает известные системные (например, горутины самого тест-раннера, `testing`), и фейлит тест, если остались чужие. Полезно добавить опции игнорирования для библиотечных фоновых горутин: ```go goleak.VerifyNone(t, goleak.IgnoreTopFunction("github.com/some/lib.(*Pool).run"), ) ``` ### Профилактика 1. **context.Context во всех долгоживущих горутинах** — единый стандарт отмены. Первый `select`-case всегда `<-ctx.Done()`. 2. **Документировать контракт канала** — комментарием прямо у объявления: кто пишет, кто закрывает. Правило Go: **закрывает всегда отправитель**, и только один; нельзя закрывать канал из нескольких мест и нельзя писать в закрытый канал (паника). 3. **Buffered-каналы для fire-and-forget** — если результат может быть никому не нужен (отправитель ушёл по таймауту), буфер на 1 спасает горутину от вечной блокировки на `send`: ```go func fetch(ctx context.Context) (Result, error) { ch := make(chan Result, 1) // буфер 1 → send не заблокируется, даже если читатель ушёл go func() { ch <- doExpensiveCall() // не утечёт благодаря буферу }() select { case r := <-ch: return r, nil case <-ctx.Done(): return Result{}, ctx.Err() // горутина завершится сама, записав в буфер } } ``` 4. **errgroup** — для группы связанных горутин с распространением ошибки и отменой: ```go import "golang.org/x/sync/errgroup" func process(ctx context.Context, urls []string) error { g, ctx := errgroup.WithContext(ctx) // ctx отменяется при первой ошибке g.SetLimit(8) // ограничение конкурентности (Go 1.20+) for _, u := range urls { u := u g.Go(func() error { return fetch(ctx, u) // при ошибке любой — ctx.Done() закроется для всех }) } return g.Wait() // ждёт всех и возвращает первую ошибку } ``` #### Worker pool с правильной отменой ```go func WorkerPool(ctx context.Context, jobs <-chan Job, workers int) <-chan Result { results := make(chan Result) var wg sync.WaitGroup wg.Add(workers) for i := 0; i < workers; i++ { go func() { defer wg.Done() for { select { case <-ctx.Done(): return // отмена — все воркеры выходят case j, ok := <-jobs: if !ok { return // канал jobs закрыт — нормальное завершение } select { case results <- process(ctx, j): // НЕ забываем select и тут! case <-ctx.Done(): return // иначе утечём на send, если читатель results ушёл } } } }() } // Отдельная горутина закрывает results ПОСЛЕ всех воркеров — единственный закрыватель go func() { wg.Wait() close(results) }() return results } ``` Ключевые моменты этого паттерна: - Выход по `ctx.Done()` **и** по закрытию `jobs` — два независимых пути завершения. - Отправка в `results` тоже обёрнута в `select` с `ctx.Done()` — иначе при отмене воркер залипнет на `send`, если потребитель `results` уже ушёл. - `close(results)` делает **одна** горутина после `wg.Wait()` — соблюдён инвариант «один закрыватель». ## Подводные камни / gotchas - **`time.Tick` течёт**: возвращает канал, но не даёт его остановить. Используйте `t := time.NewTicker(d); defer t.Stop()`. - **`time.After` в цикле**: на каждой итерации `select` создаёт новый таймер, который живёт до своего срабатывания. В горячем цикле это утечка памяти таймеров (до Go 1.23, где сборка улучшена) — используйте переиспользуемый `Timer` с `Reset`. - **Отправка в `results` без `select` на отмену** — самая частая «утечка во второй половине» worker pool (исправлено в примере выше). - **`defer` в зависшей горутине не сработает** — `Close()`/`Unlock()`/`Done()` не выполнятся, потянув за собой утечку FD/мьютексов/WG. - **Закрытие канала из нескольких горутин** → паника `close of closed channel` или гонка. Закрыватель строго один. - **Range по каналу, который никто не закроет**, висит вечно — это утечка, а не deadlock (рантайм не видит её, если другие горутины активны). - **goleak ловит не сразу**: горутина может завершаться асинхронно; goleak делает несколько ретраев с задержкой, но горутины с фоновым `time.Sleep` могут дать ложноположительный результат — настраивайте `IgnoreTopFunction`. - **`runtime.Goexit` и паника**: паника в горутине без `recover` роняет **весь процесс**, а не только горутину — это не «утечка», но связанная ошибка управления жизненным циклом. - **Частичный deadlock не детектируется**: `all goroutines are asleep` срабатывает, только если зависли ВСЕ. С живым HTTP-сервером утечки молча копятся. ## Вопросы на собеседовании **В:** Почему сборщик мусора не освобождает заблокированную горутину? **О:** Потому что заблокированная горутина с точки зрения рантайма жива и достижима: её G-структура и стек — это корень (root) для GC. GC собирает недостижимые объекты в куче, но горутина не «недостижима» — она запаркована планировщиком в состоянии `_Gwaiting` и может теоретически быть разбужена (если на канал кто-то пошлёт). Рантайм не делает анализа «а пошлёт ли кто-нибудь когда-нибудь» — это была бы неразрешимая задача. Поэтому горутина живёт, пока её функция не вернётся, и весь захваченный ею через замыкание граф объектов тоже остаётся живым. **В:** В чём разница между `debug=1` и `debug=2` у goroutine profile, и когда что использовать? **О:** `debug=1` агрегирует горутины по идентичным стекам и показывает счётчик для каждой группы — удобно, когда тысячи горутин висят на одном месте и нужно быстро увидеть «горячий» стек. `debug=2` печатает каждую горутину отдельно, с её ID, возрастом (`X minutes`) и состоянием (`chan receive`, `semacquire` и т.д.) — это формат, идентичный дампу по `SIGQUIT`. Для поиска утечки начинаю с `debug=1` (где скопление?), а `debug=2` беру, когда нужны детали конкретной долгоживущей горутины или её точный возраст. Ещё мощнее — diff двух профилей через `go tool pprof -base`, который покажет именно растущий стек. **В:** Почему буферизация канала на 1 элемент спасает от утечки в паттерне «запрос с таймаутом»? **О:** В паттерне, где горутина считает результат и шлёт его в канал, а вызывающий код ждёт результат или `ctx.Done()`, при срабатывании таймаута вызывающий уходит и больше не читает канал. Если канал unbuffered, `send` в горутине требует рандеву с `recv`, которого уже не будет — горутина виснет навсегда. Буфер на 1 позволяет `send` завершиться без читателя: значение ложится в буфер, горутина возвращается и собирается GC вместе с каналом. Размер ровно 1, потому что отправка одна; больше не нужно. Это идиома «fire-and-forget result». **В:** Кто должен закрывать канал и почему это важно для утечек? **О:** Канал закрывает отправитель, и только один. Причины: запись в закрытый канал — паника, повторный `close` — паника, а закрытие из нескольких мест порождает гонку. Для утечек это важно с двух сторон: (1) если канал никто не закроет, все читатели в `range`/`recv` зависнут навсегда — утечка; (2) закрытие канала — это и есть сигнал «данных больше не будет», который позволяет читателям корректно выйти из цикла (`v, ok := <-ch; if !ok { return }`). При нескольких отправителях используют отдельную координирующую горутину, которая делает `wg.Wait(); close(ch)` — как в worker pool. **В:** Как `errgroup.WithContext` помогает избегать утечек? **О:** `errgroup.WithContext` возвращает группу и производный контекст. При первой ошибке любой из `g.Go`-функций (или при отмене родительского контекста) этот производный контекст отменяется, и его `Done()` закрывается для всех остальных горутин. Если все горутины слушают этот `ctx` в своих `select`, они дружно завершаются — это автоматический fan-out отмены. `g.Wait()` дожидается всех и возвращает первую ошибку. Без errgroup пришлось бы вручную городить `context.WithCancel` + `WaitGroup` + сбор первой ошибки через мьютекс. Важная оговорка: errgroup отменяет контекст, но сами горутины обязаны его слушать — если функция игнорирует `ctx`, отмена не сработает. **В:** Рантайм паникует `all goroutines are asleep - deadlock`. Всегда ли это срабатывает при дедлоке? **О:** Нет. Этот детектор срабатывает, только если **все** горутины процесса находятся в заблокированном состоянии и нет ни одной runnable. Если в программе крутится хоть одна активная горутина — типично HTTP-сервер в `Accept`/`netpoll` или фоновый тикер — рантайм считает, что прогресс возможен, и частичный дедлок на подмножестве горутин остаётся незамеченным. Именно поэтому в долгоживущих сервисах утечки горутин накапливаются молча, и их видно только по росту `NumGoroutine`/pprof, а не по падению с deadlock. **В:** Чем `time.Tick` опасен и как сделать правильно? **О:** `time.Tick(d)` возвращает канал, но не возвращает сам `*Ticker`, поэтому его невозможно остановить через `Stop()`. Базовый тикер и горутина-источник тиков живут до конца процесса — это утечка, если такой паттерн используется в коде с ограниченным временем жизни. Правильно: `t := time.NewTicker(d); defer t.Stop()` и читать из `t.C`. `time.Tick` допустим только для тикеров, живущих весь срок программы, и даже тогда лучше явный `NewTicker` для читаемости. **В:** Как встроить защиту от утечек в CI, чтобы не ловить их в проде? **О:** Использую `go.uber.org/goleak`: в каждом конкурентном пакете добавляю `TestMain` с `goleak.VerifyTestMain(m)` либо `defer goleak.VerifyNone(t)` в конкретных тестах. goleak делает снимок горутин после теста, фильтрует системные и фейлит, если остались чужие, с показом их стеков. Для фоновых горутин библиотек добавляю `goleak.IgnoreTopFunction`. Дополнительно — экспортирую `runtime.NumGoroutine()` как метрику (`go_goroutines` в Prometheus) и ставлю алерт на монотонный рост, плюс держу `net/http/pprof` доступным в проде на внутреннем порту для разбора инцидентов через goroutine diff. ## На что копают на senior+ - **Понимание разницы «deadlock vs leak»**: способны ли вы объяснить, почему частичный дедлок не детектируется рантаймом и почему утечки в сервисах молчат. - **Контракт каналов**: кто закрывает, что происходит при send/close в закрытый канал, как координировать close при N отправителях. - **Двусторонняя отмена в worker pool**: видите ли вы, что отправка в `results` тоже должна быть под `select` с `ctx.Done()`, а не только чтение `jobs`. - **Стоимость утечки**: знаете ли, что течёт не только стек (2 КБ+), но и захваченный граф объектов и внешние ресурсы (FD, conn), потому что `defer` не выполнится. - **Инструментарий под нагрузкой**: умение снять goroutine diff в проде, читать состояния `gopark` в дампе, отличить `semacquire` (мьютекс/WG) от `chan receive`. - **goleak в CI и его ограничения**: настройка игнор-листов, ложные срабатывания на асинхронно завершающихся горутинах. - **Таймеры**: знание про `time.Tick`, `time.After` в цикле и поведение таймеров до/после Go 1.23. - **errgroup семантика**: что `WithContext` отменяет контекст, но горутины обязаны его слушать; `SetLimit` для конкурентности; первая ошибка vs все ошибки.