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/ > Модуль: Core Go · Уровень: Senior ## TL;DR `panic` начинает раскрутку стека текущей горутины, по пути выполняя зарегистрированные `defer`. `recover` останавливает раскрутку и возвращает значение паники, но **работает только при прямом вызове из отложенной функции** — иначе возвращает `nil` и эффекта не имеет. Паника в любой горутине, не пойманная её собственным `defer`/`recover`, убивает весь процесс — поймать её из другой горутины нельзя. Часть runtime-ошибок (`nil deref`, `index out of range`) — это обычные паники и их можно восстановить; часть (`concurrent map writes`, `deadlock`, нехватка памяти) — это fatal errors и они принципиально не recoverable. ## Теория ### Что такое panic и recover на уровне семантики `panic(v)` — это не «бросок исключения» в классическом смысле. Это запуск процедуры аварийной раскрутки стека (stack unwinding) **текущей горутины**: 1. Останавливается нормальное выполнение функции. 2. Начинают по очереди (LIFO) исполняться все `defer`, зарегистрированные в этой и вышестоящих функциях. 3. Если ни один из `defer` не вызвал `recover`, раскрутка доходит до вершины стека горутины, рантайм печатает сообщение паники + стектрейс и завершает процесс с кодом 2 (через `fatalpanic` → `exit(2)`). 4. Если `defer` вызвал `recover`, паника считается «погашенной»: раскрутка останавливается, и выполнение продолжается **в вызывающей функции того defer'а** (как будто эта функция нормально вернулась). `recover()` возвращает: - значение, переданное в `panic(...)`, если в данный момент горутина паникует и `recover` вызван корректно; - `nil` — если паники нет, ИЛИ если `recover` вызван не из defer'а напрямую, ИЛИ если в `panic(nil)` передали nil (с Go 1.21 это превратилось в `*runtime.PanicNilError`, чтобы убрать неоднозначность — см. ниже). ### recover работает ТОЛЬКО напрямую из defer Это ключевой и часто проваливаемый на собесе момент. `recover` имеет эффект, только когда: - он вызван внутри функции, которая запущена механизмом `defer`, и - вызван **непосредственно** в теле этой отложенной функции, а не во вложенном вызове. ```go func bad() { defer func() { helper() // recover внутри helper НЕ сработает }() panic("boom") } func helper() { if r := recover(); r != nil { // r == nil, паника НЕ погашена fmt.Println("recovered:", r) } } ``` ```go func good() (err error) { defer func() { if r := recover(); r != nil { // прямой вызов из defer — работает err = fmt.Errorf("recovered: %v", r) } }() panic("boom") } ``` Почему так? `recover` смотрит на runtime-структуры текущей горутины (`g`) и проверяет, что он вызван ровно из той функции, что зарегистрирована как defer для паникующего фрейма. Под капотом компилятор для `recover` передаёт «caller PC/SP», и рантайм (`gorecover`) сверяет, что вызывающий фрейм — это именно фрейм исполняемого сейчас `_defer`, у которого `started == true`, а его `_panic` ещё активна. Если вызов пришёл из произвольной вложенной функции — условие не выполняется, и возвращается `nil`. ### Структуры _panic и _defer под капотом Каждая горутина (`runtime.g`) хранит два связных списка-стека: - `g._defer` — список отложенных вызовов (`runtime._defer`), LIFO. - `g._panic` — список активных паник (`runtime._panic`), тоже стек (re-panic вложен в обработку предыдущей). Упрощённо (актуально для современных версий рантайма): ```go type _defer struct { started bool // defer уже начал исполняться (в ходе паники) heap bool // аллоцирован в куче (open-coded defer — на стеке/в регистрах) sp uintptr // стек-поинтер фрейма, который зарегистрировал defer pc uintptr // адрес возврата fn func() // сама отложенная функция link *_defer // следующий в списке } type _panic struct { arg any // аргумент panic(...) link *_panic // предыдущая паника (для re-panic / вложенных) recovered bool // была ли погашена recover'ом aborted bool // прервана (например, Goexit поверх паники) // + поля для goexit, sp/pc, reraise и т.д. } ``` Алгоритм раскрутки (`runtime.gopanic`): 1. Создаётся `_panic`, кладётся в голову `g._panic`. 2. В цикле берётся очередной `_defer` из `g._defer`, помечается `started = true`, в нём запоминается ссылка на текущую `_panic`. 3. Отложенная функция вызывается. Если внутри неё происходит `recover`, он выставляет `p.recovered = true`. 4. После возврата из defer'а `gopanic` проверяет `p.recovered`. Если true — вызывается `recovery`, которая «перематывает» стек на sp/pc того фрейма, что зарегистрировал defer, и возвращает управление туда (как нормальный return). Если false — переходим к следующему defer'у. 5. Если defer'ы кончились — `fatalpanic`: печать и `exit(2)`. Про **open-coded defers** (с Go 1.14): для горячего пути компилятор не аллоцирует `_defer` в куче, а инлайнит логику defer'ов прямо в функцию с битовой маской «какие defer'ы активны». Но при панике рантайму всё равно нужно их найти — для этого в метаданных функции (`funcdata`) лежит информация, позволяющая `gopanic` восстановить список открыто-кодированных defer'ов. Это даёт ~near-zero стоимость defer в обычном потоке и сохраняет корректность при панике. ### Раскрутка стека: что важно - defer'ы выполняются **в порядке LIFO** по мере раскрутки. - Именованные возвращаемые значения можно поменять из defer'а — это единственный способ «вернуть error» после recover (см. `good()` выше). - Раскрутка идёт только по текущей горутине. Фреймы других горутин не трогаются. ### Паника и горутины Паника изолирована в рамках своей горутины, но её последствия — нет. ```go func main() { go func() { panic("in goroutine") // никто не recover'ит }() time.Sleep(time.Second) // программа уже мертва: процесс завершён с кодом 2 } ``` - **Нельзя** поймать панику чужой горутины из `main` или из любой другой горутины. У каждой горутины свой стек и свой список `_defer`/`_panic`. - Любая непойманная паника в **любой** горутине завершает **весь** процесс. Это сознательное проектное решение: невосстановленная паника = сломанный инвариант, продолжать опасно. - Практический вывод: если вы запускаете горутину, которая может паниковать (особенно из недоверенного/плагинного кода), оборачивайте её тело в `defer recover()` **внутри самой горутины**. ```go func safeGo(f func()) { go func() { defer func() { if r := recover(); r != nil { log.Printf("goroutine panic: %v\n%s", r, debug.Stack()) } }() f() }() } ``` ### Когда panic уместен - **Невосстановимые состояния**: нарушен инвариант, при котором продолжать бессмысленно или опасно (повреждение данных). - **Программерские баги**: «этого не может быть» — например `default:` в switch по перечислению, которое должно быть исчерпывающим. Лучше упасть громко, чем тихо продолжить в неверном состоянии. - **Инициализация**: в `init()` или при старте, когда без ресурса программа всё равно не сможет работать (`regexp.MustCompile`, `template.Must`, парсинг обязательного конфига). Падение на старте лучше, чем падение в проде через час. - **Внутренний control-flow в пределах пакета** (редко, осознанно) — см. ниже про парсеры. Правило большого пальца: **ошибки, которые вызывающий может разумно обработать → возвращаем `error`. Баги и невозможные состояния → `panic`.** ### Почему паники в библиотеках обычно зло Публичный API библиотеки не должен паниковать на «обычных» ошибках ввода — это нарушает контроль над потоком исполнения у пользователя и ломает композицию. Исключения, признанные сообществом нормальными: - `Must*`-функции (`regexp.MustCompile`, `template.Must`): паникуют только на аргументах, известных на этапе компиляции/инициализации, где ошибка = баг программиста. - Паника на полностью невалидном использовании API (например, отрицательная capacity у `make` — это рантайм). - Внутренняя паника, которая **не пересекает границу пакета**: пакет паникует внутри себя для control-flow, но на публичной границе ловит её и конвертирует в `error` (так делает `encoding/json`). Важно: `json.Marshal`/`Unmarshal` **не паникует** на пользовательских данных — он возвращает `error`. Внутри он использует панику как control-flow (`jsonError`), но `recover`ит её в `marshal`/`unmarshal` и отдаёт наружу как обычную ошибку. Это эталонный паттерн «паника внутри — error снаружи». ### Re-panic В defer'е после recover можно перевозбудить панику — паникнуть снова. Применяется, когда вы хотите обработать только определённый класс паник, а остальные пробросить дальше. ```go defer func() { r := recover() if r == nil { return } if e, ok := r.(MyExpectedError); ok { handle(e) return } panic(r) // не наша паника — пробрасываем дальше }() ``` Под капотом: новая `panic` создаёт новый `_panic` и связывает с предыдущим через `link`. В стектрейсе вы увидите оба: «panic: ... [recovered]» и затем «panic: ...». ### Паника как control flow (антипаттерн, но встречается) Иногда панику используют как «длинный return» через много уровней рекурсии — типично в рекурсивных парсерах/интерпретаторах, где протаскивать `error` через каждый уровень громоздко. Канонический пример — парсер из «Go Programming Language» (Donovan/Kernighan) и внутренности `encoding/json`, `text/template`, `go/...`. Правила, если уж делаете так: - паника должна иметь **приватный тип** (sentinel), чтобы не перехватить чужую; - она **никогда** не должна покидать пакет — на границе обязательно `recover` и конвертация в `error`; - любую «не свою» панику в этом recover нужно re-panic'нуть. ### Runtime-паники: что можно и нельзя восстановить Runtime сам генерирует паники для ряда ошибок. Их тип — `runtime.Error` (интерфейс), конкретно часто `*runtime.TypeAssertionError`, `runtime.boundsError` и т.п. **Recoverable (обычные паники, тип `runtime.Error`):** | Ошибка | Тип/сообщение | |---|---| | Разыменование nil | `invalid memory address or nil pointer dereference` (`runtime.Error`, signal SIGSEGV перехватывается рантаймом) | | Выход за границы | `index out of range [i] with length n` | | Срез вне диапазона | `slice bounds out of range` | | Неверное приведение типа | `interface conversion: ...` (`*runtime.TypeAssertionError`) | | Деление на ноль (целые) | `integer divide by zero` | | Закрытие закрытого канала / запись в закрытый | `close of closed channel` / `send on closed channel` | | Отрицательная длина у make | `runtime: ... makeslice: len out of range` | **НЕ recoverable (fatal errors / throw — обходят механизм panic):** | Ситуация | Почему нельзя | |---|---| | `concurrent map writes` | Рантайм детектит гонку на map и вызывает `throw`, а не `panic`. `throw` → `fatalthrow`, defer'ы НЕ выполняются, `recover` бессилен. | | `concurrent map read and write` | То же — `throw`. | | Deadlock (`all goroutines are asleep`) | Обнаруживается планировщиком, `throw`. | | Out of memory | `throw`. | | Stack overflow | `throw`. | | Гонка, найденная race detector'ом | Завершает процесс, не паника. | | `nil map` assignment (`assignment to entry in nil map`) | Это, наоборот, **обычная паника** и recoverable — не путать с concurrent map. | Принцип: всё, что рантайм считает «программа в принципе в неконсистентном/небезопасном состоянии и продолжать нельзя» → `throw`/`fatal` → не ловится. Всё, что «локальная логическая ошибка» → `panic` → ловится. ```go // concurrent map write — recover НЕ поможет func main() { defer func() { recover() }() // бесполезно m := map[int]int{} for i := 0; i < 8; i++ { go func() { for { m[1] = 1 } }() } select {} // fatal error: concurrent map writes } ``` ### recover в HTTP middleware Классическое легитимное применение recover на верхнем уровне обработки запроса: одна паника в одном хендлере не должна ронять весь сервер. ```go func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { // ErrAbortHandler — спец-значение net/http, его пробрасываем if rec == http.ErrAbortHandler { panic(rec) } log.Printf("panic: %v\n%s", rec, debug.Stack()) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } ``` Нюансы: - `net/http` сервер **сам** имеет recover на каждый запрос — паника в хендлере не уронит сервер по умолчанию, но соединение разрывается, а лог куцый. Свой middleware даёт нормальный ответ клиенту и полный стектрейс. - `http.ErrAbortHandler` — специальная паника, которой хендлер сигнализирует «прервать без логирования»; её нужно пробрасывать (re-panic), а не глотать. - recover в middleware **не поймает** панику из горутины, которую хендлер запустил сам (`go ...`) — она в другой горутине и уронит весь процесс. - Если уже начали писать тело ответа (`WriteHeader`), `http.Error` после паники не сможет поменять статус-код. ## Подводные камни / gotchas - **recover не из defer'а напрямую** — самая частая ошибка. `recover()` в обычном коде или во вложенном вызове внутри defer'а вернёт `nil` и ничего не погасит. - **Паника в горутине игнорирует чужие recover'ы.** `defer recover()` в `main` не спасёт от паники в `go func()`. Ловить надо внутри самой горутины. - **`panic(nil)`**: до Go 1.21 это приводило к тому, что `recover()` возвращал `nil`, и нельзя было отличить «не паниковали» от «паниковали с nil». С Go 1.21 `panic(nil)` превращается в `panic(&runtime.PanicNilError{})`, и `recover` вернёт ненулевое значение (поведение управляется `GODEBUG=panicnil=1` для совместимости). - **concurrent map writes не ловится** — частый сюрприз: люди оборачивают всё в recover и удивляются, что процесс всё равно падает. Это `throw`, не `panic`. - **recover «съедает» все паники**, включая баги. `defer func(){ recover() }()` без анализа значения маскирует реальные ошибки. Всегда логируйте и/или re-panic неожиданное. - **Именованный return обязателен для возврата ошибки из recover.** Без именованного результата defer не сможет повлиять на возвращаемое значение. - **`os.Exit` не выполняет defer'ы** — и, соответственно, никакой recover. Это не паника, это немедленный выход. - **Goexit + panic**: `runtime.Goexit` тоже раскручивает стек и исполняет defer'ы; если в defer'е во время Goexit паникнуть и не восстановить — fatal. Взаимодействие тонкое, всплывает в тестах (`t.FailNow` использует Goexit). - **debug.Stack() в recover** даёт стек на момент вызова в defer'е (уже частично раскрученный), но обычно его достаточно; для точного места паники лучше `debug.PrintStack` сразу или настроить `GOTRACEBACK`. - **panic печатает не error, а значение через его форматирование.** Паника произвольным типом усложняет анализ; для control-flow используйте типизированную приватную обёртку. ## Вопросы на собеседовании **В:** Почему `recover` нужно вызывать именно напрямую из defer-функции, а не из вложенного вызова? **О:** Потому что `gorecover` в рантайме сверяет caller'а recover'а с фреймом текущего исполняемого `_defer`, у которого `started=true` и который связан с активной `_panic`. При прямом вызове из отложенной функции это совпадает, и `recover` помечает `_panic.recovered=true`. При вызове из вложенной функции фрейм не совпадает — рантайм считает, что recover вызван «не там», и возвращает `nil`, паника продолжает раскрутку. **В:** Что происходит при панике в горутине без recover? Можно ли поймать её снаружи? **О:** Раскручивается стек этой горутины, исполняются её defer'ы; если ни один не сделал recover, рантайм вызывает `fatalpanic` и завершает **весь** процесс с кодом 2. Поймать из другой горутины нельзя — у каждой горутины свой стек и свои `_defer`/`_panic`. Ловить нужно внутри самой горутины. **В:** Какие runtime-ошибки можно восстановить через recover, а какие нет? **О:** Восстанавливаются обычные паники типа `runtime.Error`: nil dereference, index/slice out of range, неудачное type assertion, деление на ноль, send/close на закрытом канале, assignment to nil map. НЕ восстанавливаются fatal errors, которые рантайм возбуждает через `throw`/`fatalthrow`: concurrent map writes/read-write, deadlock, OOM, stack overflow, находки race detector'а. `throw` не исполняет defer'ы и игнорирует recover, потому что состояние программы считается небезопасным. **В:** Зачем `encoding/json` использует панику внутри, если публично возвращает error? **О:** Внутри парсинга/кодирования удобно прервать глубокую рекурсию одной «длинной» паникой приватного типа (`jsonError`), не протаскивая error через каждый уровень. На границе пакета (`marshal`/`unmarshal`) стоит `recover`, который ловит только свой приватный тип, конвертирует в `error`, а любую чужую панику re-panic'ит. Это эталон: паника как внутренний control-flow, error — как внешний контракт. **В:** Что вернёт `recover()` и в каких случаях `nil`? **О:** Возвращает значение, переданное в `panic`, если горутина сейчас паникует и recover вызван корректно из defer'а. Возвращает `nil`, если: паники нет; recover вызван не из defer'а напрямую; либо (до Go 1.21) была `panic(nil)`. С Go 1.21 `panic(nil)` даёт `*runtime.PanicNilError`, и recover вернёт его, а не nil. **В:** Как корректно превратить панику в возвращаемую ошибку? **О:** Использовать именованный возвращаемый параметр и в defer'е сделать `if r := recover(); r != nil { err = ... }`. Только именованный результат можно изменить из отложенной функции; обычный return-фрейм уже зафиксирован. Желательно различать `runtime.Error`, свои типы и неизвестные паники (последние лучше re-panic). **В:** Как устроена связь `_panic` и `_defer` в рантайме при раскрутке? **О:** У горутины два стека: `g._defer` и `g._panic`. `gopanic` кладёт новый `_panic` в голову, затем в цикле берёт defer'ы (LIFO), помечает `started`, исполняет. После каждого defer'а проверяет `recovered`: если true — вызывает `recovery`, перематывает SP/PC на фрейм, зарегистрировавший defer, и возвращает туда управление. Re-panic создаёт новый `_panic`, связанный через `link`. Если defer'ы исчерпаны без recover — `fatalpanic`. **В:** В чём смысл recover-middleware в HTTP, если сервер сам ловит паники? **О:** `net/http` действительно recover'ит панику на запрос, но отдаёт клиенту разорванное соединение и пишет скудный лог. Собственный middleware даёт контролируемый ответ (500), полный стектрейс через `debug.Stack()`, метрики/алерты, и корректно пробрасывает `http.ErrAbortHandler`. Важно помнить, что middleware не поймает панику из горутины, запущенной хендлером. **В:** Чем `panic`/`recover` отличается от исключений в Java/C++? **О:** Это не штатный механизм обработки ошибок и не предназначен для control-flow между модулями. Нет иерархии «классов исключений», нет catch по типу на уровне языка (только type switch внутри recover), recover ограничен defer'ом и текущей горутиной, а идиоматичный способ сообщать об ошибках — возвращаемый `error`. Паника зарезервирована для багов и невосстановимых состояний. ## На что копают на senior+ - **Понимание `panic` vs `throw`.** Senior чётко разделяет recoverable паники (`runtime.Error`) и fatal errors (`fatalthrow` без defer'ов). Follow-up: «почему concurrent map write нельзя поймать, а nil map assignment можно?» — ответ про safety vs локальную логическую ошибку. - **Open-coded defers.** Знание, что с Go 1.14 defer в горячем пути почти бесплатен и как рантайм восстанавливает их при панике через `funcdata`, отличает поверхностное знание от глубокого. - **Стоимость и аллокации.** `_defer` в куче vs на стеке/в регистрах; влияние паники на инлайнинг; почему добавление recover может «расхолодить» функцию (мешает оптимизациям defer'ов). - **Корректный re-panic и фильтрация.** Senior никогда не делает «глухой» `recover()`, который глотает всё; он типизирует свою панику и пробрасывает чужую. Follow-up: «что будет в стектрейсе после re-panic» (`[recovered]` + новая паника, связанные через `link`). - **Горутины и graceful degradation.** Обсуждение worker-pool'ов: каждая worker-горутина должна иметь свой recover, иначе один плохой таск роняет весь сервис; как при этом не потерять задачу и залогировать. - **`panic(nil)` и эволюция семантики (Go 1.21, `GODEBUG=panicnil`).** Знание историй совместимости показывает, что человек следит за рантаймом. - **Goexit/тесты.** Тонкое взаимодействие `runtime.Goexit`, `t.FailNow`, паник в defer'ах — всплывает при отладке зависающих/падающих тестов. - **Граница пакета как контракт.** Способность сформулировать правило «паника не пересекает публичную границу пакета, кроме осознанных Must*» и привести `encoding/json` как пример.