Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Встраивание структур и интерфейсов (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/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Замыкания и переменная цикла (Go 1.22): https://go.vbloher.org/docs/01-core-go/closures-loopvar/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - encoding/json и теги структур: https://go.vbloher.org/docs/01-core-go/json-encoding/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Модули, версионирование и зависимости: https://go.vbloher.org/docs/01-core-go/go-modules/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - sync.Map и sync.Pool: https://go.vbloher.org/docs/02-concurrency/sync-map-pool/ - errgroup: группа горутин с ошибкой и отменой: https://go.vbloher.org/docs/02-concurrency/errgroup/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Сборщик мусора 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/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - 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/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Backend: https://go.vbloher.org/docs/05-backend/ - Структура Go-проекта: https://go.vbloher.org/docs/05-backend/project-layout/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - Чистая архитектура и внедрение зависимостей в Go: https://go.vbloher.org/docs/05-backend/clean-architecture/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - 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/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Observability: https://go.vbloher.org/docs/09-observability/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - 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/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - DevOps: https://go.vbloher.org/docs/11-devops/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ # errgroup: группа горутин с ошибкой и отменой > Модуль: Concurrency · Уровень: Senior ## TL;DR `golang.org/x/sync/errgroup` — это `WaitGroup`, который умеет две недостающие вещи: возвращать **первую ненулевую ошибку** и **отменять контекст** остальных горутин при её появлении. `errgroup.WithContext(ctx)` возвращает группу и производный контекст, который отменяется, когда первая функция вернула ошибку или когда `Wait` завершился. `SetLimit(n)` ограничивает число одновременно работающих горутин, превращая группу в пул; `TryGo` пытается запустить без блокировки. Ключевое условие пользы: **горутины обязаны сами слушать `ctx.Done()`** — отмена не прерывает работающий код, она лишь сообщает о необходимости остановиться. ## Простыми словами `errgroup` — это `WaitGroup`, который умеет возвращать ошибку и останавливать остальных. Задача, ради которой он придуман, встречается постоянно: сходить параллельно в три сервиса и собрать ответ. С обычным `WaitGroup` придётся руками завести канал под ошибки, придумать, что делать со второй и третьей ошибкой, отдельно создать контекст с отменой и не забыть его дёрнуть. С `errgroup` это выглядит так: ```go g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return fetchUser(ctx) }) g.Go(func() error { return fetchOrders(ctx) }) if err := g.Wait(); err != nil { return err // первая случившаяся ошибка, остальные горутины уже получили сигнал отмены } ``` Важно понять, чего он **не** делает. Отмена контекста ничего не прерывает силой — горутина, которая крутит цикл и не смотрит в `ctx.Done()`, продолжит работать как ни в чём не бывало. Контекст только сообщает: «результат больше не нужен, сворачивайся». **Если ты с другого языка.** Ближайший аналог — `asyncio.TaskGroup` из Python 3.11 или структурированная конкурентность из Kotlin. Отличие: в Go отменяемость не встроена в рантайм, поэтому каждая функция обязана явно проверять контекст. Компилятор об этом не напомнит. **Словарь этой статьи:** - **производный контекст** — контекст, созданный от родителя и отменяемый вместе с ним или по ошибке в группе; - **`SetLimit`** — ограничение числа одновременно работающих горутин; - **структурированная конкурентность** — принцип «горутина не переживает область, которая её породила». ## Теория ### Чего не хватает WaitGroup ```go var wg sync.WaitGroup var mu sync.Mutex var firstErr error for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() if err := fetch(u); err != nil { mu.Lock() if firstErr == nil { firstErr = err } mu.Unlock() } }(url) } wg.Wait() ``` Здесь три проблемы: ошибки собираются руками через мьютекс, при первой ошибке остальные продолжают работать впустую, и нет никакого способа их остановить. `errgroup` закрывает всё это. ### Базовое использование ```go import "golang.org/x/sync/errgroup" func fetchAll(ctx context.Context, urls []string) error { g, ctx := errgroup.WithContext(ctx) for _, url := range urls { url := url // на Go < 1.22 обязательно g.Go(func() error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("fetch %s: %w", url, err) } defer resp.Body.Close() _, err = io.Copy(io.Discard, resp.Body) return err }) } return g.Wait() } ``` Что здесь происходит: - `g.Go` запускает горутину и берёт на себя учёт; - если любая функция вернула ошибку, производный `ctx` отменяется, и все запросы, созданные через `NewRequestWithContext`, немедленно прерываются; - `g.Wait()` дожидается всех и возвращает **первую** ошибку (по времени возникновения, не по порядку запуска). Обрати внимание на переприсваивание `ctx := ...`: производный контекст намеренно затеняет исходный, чтобы случайно не передать в горутину неотменяемый родительский. ### Сбор результатов `g.Go` принимает функцию без результата, поэтому значения возвращают через заранее выделенные ячейки: ```go results := make([]Result, len(ids)) // каждая горутина пишет в свой индекс — гонки нет g, ctx := errgroup.WithContext(ctx) for i, id := range ids { i, id := i, id g.Go(func() error { r, err := load(ctx, id) if err != nil { return err } results[i] = r return nil }) } if err := g.Wait(); err != nil { return nil, err } ``` Запись в разные индексы одного слайса из разных горутин безопасна: элементы не пересекаются в памяти, а `g.Wait()` создаёт нужное отношение happens-before перед чтением. Менять сам слайс (`append`) при этом, разумеется, нельзя. ### Ограничение параллелизма: SetLimit Без ограничения `errgroup` запустит столько горутин, сколько задач. На десяти тысячах URL это десять тысяч одновременных соединений — верный способ получить отказ от внешнего сервиса и исчерпать порты. ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // не больше 10 одновременно for _, id := range ids { id := id g.Go(func() error { // блокируется, пока не освободится слот return process(ctx, id) }) } return g.Wait() ``` `SetLimit` превращает группу в пул воркеров без единой дополнительной строки. Два правила: вызывать его нужно **до** первого `g.Go`, и `SetLimit(-1)` снимает ограничение. Неблокирующий вариант — `TryGo`: он возвращает `false`, если свободных слотов нет, и не запускает функцию. ### Когда отменяется контекст Производный контекст из `WithContext` отменяется в двух случаях: когда первая функция вернула ненулевую ошибку, и когда `Wait` вернул управление. Второе важно для утечек: ```go func handler(ctx context.Context) error { g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return worker(ctx) }) return g.Wait() // после возврата ctx отменён — фоновые задачи не переживут функцию } ``` Это и есть структурированная конкурентность: горутины не разбегаются за пределы породившей их области. ### Что отмена НЕ делает ```go g.Go(func() error { for i := 0; i < 1_000_000_000; i++ { heavyComputation(i) // ctx никто не смотрит } return nil }) ``` Такая горутина доработает до конца, чего бы там ни отменяли. Правильный вариант — регулярная проверка: ```go g.Go(func() error { for i := 0; i < 1_000_000_000; i++ { if i%1000 == 0 { select { case <-ctx.Done(): return ctx.Err() default: } } heavyComputation(i) } return nil }) ``` То же касается вызовов, не принимающих контекст: старые библиотеки, `os/exec` без `CommandContext`, драйверы без контекстных методов — всё это отменой не остановить. ### Только первая ошибка `Wait` возвращает одну ошибку — ту, что случилась первой. Остальные теряются. Если нужны все (например, при валидации, где каждая задача проверяет свой кусок), собирай их самостоятельно: ```go var ( mu sync.Mutex errs []error ) g := new(errgroup.Group) // без WithContext: одна ошибка не должна отменять остальных for _, check := range checks { check := check g.Go(func() error { if err := check(); err != nil { mu.Lock() errs = append(errs, err) mu.Unlock() } return nil // всегда nil, чтобы группа не останавливалась }) } _ = g.Wait() return errors.Join(errs...) ``` ### errgroup против WaitGroup.Go (Go 1.25) В Go 1.25 у `sync.WaitGroup` появился метод `Go`, снимающий классическую ошибку с `Add`/`Done`: ```go var wg sync.WaitGroup wg.Go(func() { work() }) wg.Wait() ``` Это удобно, но решает только учёт горутин. Ошибок и отмены там по-прежнему нет, поэтому `errgroup` не устарел: `WaitGroup.Go` — для «просто дождаться», `errgroup` — для «дождаться, получить ошибку и остановить остальных». ### Паника внутри группы `errgroup` **не** перехватывает панику: она распространится обычным образом и уронит процесс. Если задачи выполняют недоверенный или сложный код, восстановление пишут самостоятельно: ```go g.Go(func() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) } }() return risky(ctx) }) ``` ## Подводные камни / gotchas - **Забыть переприсвоить `ctx`.** `g, gctx := errgroup.WithContext(ctx)` и последующая передача в горутины исходного `ctx` — отмена работать не будет. - **Горутина не слушает контекст.** Отмена ничего не прерывает силой; без проверки `ctx.Done()` группа лишь дождётся всех, ничего не сэкономив. - **`SetLimit` после `Go`.** Вызов после запуска первой горутины паникует. - **Теряются все ошибки, кроме первой.** Для агрегации нужен свой сбор и `errors.Join`. - **`g.Go` блокируется при исчерпанном лимите.** Это ожидаемо, но вызов `g.Go` из горутины, которую сама же группа и ждёт, приводит к дедлоку. - **Паника не перехватывается** и убивает процесс. - **Общий слайс через `append`** — гонка. Писать можно только в заранее выделенные разные индексы. - **`errgroup` не в стандартной библиотеке**, а в `golang.org/x/sync` — это отдельная зависимость (хоть и официальная). ## Вопросы на собеседовании **В:** Чем `errgroup` лучше `WaitGroup`? **О:** Он собирает ошибку от горутин и, в варианте `WithContext`, отменяет остальных при первой же ошибке. С `WaitGroup` это пришлось бы писать руками через канал ошибок, мьютекс и отдельный контекст с отменой. **В:** Когда отменяется контекст из `WithContext`? **О:** Когда первая переданная функция вернула ненулевую ошибку либо когда вернулся `Wait`. Второе гарантирует, что запущенные горутины не переживут вызывающую функцию. **В:** Отменит ли `errgroup` горутину, которая занята тяжёлым вычислением? **О:** Нет. Отмена — это только сигнал в канале `ctx.Done()`. Функция обязана его проверять сама, иначе доработает до конца. Прервать горутину извне в Go нельзя в принципе. **В:** Как ограничить число одновременных горутин? **О:** `g.SetLimit(n)` до первого `g.Go`. После этого `g.Go` блокируется, пока не освободится слот, а `TryGo` возвращает `false` вместо ожидания. **В:** Что вернёт `Wait`, если ошиблись три горутины? **О:** Первую по времени возникновения ошибку. Остальные будут потеряны — если нужны все, их собирают самостоятельно и объединяют через `errors.Join`. **В:** Как безопасно собрать результаты из горутин группы? **О:** Выделить слайс нужной длины заранее и писать каждой горутине в свой индекс. Это безопасно, потому что элементы не пересекаются, а `Wait` устанавливает happens-before перед чтением результатов. **В:** Нужен ли `errgroup` после появления `WaitGroup.Go` в Go 1.25? **О:** Да. `WaitGroup.Go` решает только проблему корректного учёта горутин; ошибок и отмены он не даёт. ## На что копают на senior+ - Понимание, что отмена в Go кооперативная: контекст — это сигнал, а не прерывание. - Структурированная конкурентность: осознание, что после `Wait` контекст отменён и горутины не разбегаются. - Работа с параллелизмом на практике: почему безлимитный `errgroup` на большом входе — авария, и как соотносится `SetLimit` с лимитами внешнего сервиса и пула соединений. - Аккуратность с результатами: почему запись по индексам безопасна, а `append` — нет. - Follow-up: «Как отличить отмену по таймауту от отмены по ошибке соседа?» (`context.Cause`), «Что делать с паникой в задаче?», «Как совместить `errgroup` с семафором на внешний API?».