> Как остановить обработку после получения первых двух успешных запросов в Go (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Ozon
Стек: Go
> Пример ответа
Короткий ответ
Используйте errgroup с контекстом или канал с буфером и sync.Once. Самый идиоматичный способ - errgroup.WithContext(ctx), запустить N горутин, каждая отправляет результат в канал, а основная горутина читает ровно два значения и вызывает cancel(). Альтернатива - context.WithCancel вручную и select в каждой горутине. Главное - гарантировать, что после второго успеха все остальные горутины завершатся, а ресурсы освободятся.
Подробное объяснение
Задача сводится к координации конкурентных операций с ранним выходом. В Go есть несколько подходов:
-
errgroup+context- самый чистый вариант.errgroup.WithContextсоздаёт контекст, который отменяется при первой ошибке или вручную черезcancel(). Каждая горутина проверяетctx.Done()перед отправкой результата. -
Канал с буфером +
sync.Once- если нужно собрать ровно N результатов, а не отменить всё. Канал с буфером размера 2 позволяет горутинам не блокироваться, аsync.Onceгарантирует, что закрытие канала произойдёт один раз. -
context.WithCancelвручную - даёт больше контроля, но требует аккуратной работы сdefer cancel()и проверкойctx.Err().
Ключевой trade-off: errgroup удобен, когда нужно дождаться завершения всех горутин, но здесь мы хотим выйти раньше. Поэтому основная горутина читает из канала, а после второго значения вызывает cancel(). Остальные горутины при следующей проверке контекста завершатся.
Важно: если горутины выполняют блокирующие операции (например, HTTP-запросы), отмена контекста должна пробрасываться в сам запрос через http.NewRequestWithContext. Иначе горутина не завершится, пока операция не закончится естественным путём.
На практике
Типичный сценарий - параллельный опрос нескольких сервисов, где нужны первые два успешных ответа. Например, поиск по нескольким источникам: как только два источника ответили, остальные можно отменить.
Паттерн выглядит так:
- Создаём
ctx, cancel := context.WithCancel(context.Background()). - Запускаем горутины, каждая делает запрос с этим контекстом.
- Результаты отправляем в канал
results. - В основной горутине читаем из канала в цикле, считаем успешные ответы.
- После второго успеха вызываем
cancel()и выходим из цикла.
Важно обрабатывать случай, когда горутин меньше двух - тогда цикл завершится по закрытию канала или по ctx.Done().
Пример кода
GOpackage mainimport ("context""fmt""net/http""sync")func fetch(ctx context.Context, url string, results chan<- string) {req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)if err != nil {return}resp, err := http.DefaultClient.Do(req)if err != nil {return}defer resp.Body.Close()if resp.StatusCode == http.StatusOK {select {case results <- url:case <-ctx.Done():}}}func main() {ctx, cancel := context.WithCancel(context.Background())defer cancel()urls := []string{"https://api1.example.com","https://api2.example.com","https://api3.example.com","https://api4.example.com",}results := make(chan string, 2)var wg sync.WaitGroupfor _, url := range urls {wg.Add(1)go func(u string) {defer wg.Done()fetch(ctx, u, results)}(url)}var successes []stringfor len(successes) < 2 {select {case url := <-results:successes = append(successes, url)if len(successes) == 2 {cancel()}case <-ctx.Done():// контекст отменён, но мы всё равно ждём завершения горутин// чтобы не утечь ресурсы}}wg.Wait()close(results)fmt.Printf("Успешные ответы: %v\n", successes)}
Как отвечать на собеседовании
Начните с errgroup - это стандартный ответ для senior. Объясните, почему именно context.WithCancel, а не просто канал: отмена должна пробрасываться в HTTP-запросы. Упомяните, что errgroup удобен, но здесь не обязателен, так как мы не ждём ошибок - нам нужны только успехи.
Покажите понимание проблемы с блокирующими операциями: если горутина зависла на сетевом запросе, cancel() не поможет - нужен контекст в самом запросе. Это отличает senior от middle.
Расскажите про альтернативы: sync.Once для закрытия канала, буферизованный канал для неблокирующей отправки. Объясните, почему буфер размера 2 достаточен - больше двух горутин не смогут заблокироваться на отправке.
Что проверяет интервьюер
- Понимание конкурентности в Go: горутины, каналы, контекст.
- Умение координировать ранний выход из конкурентных операций.
- Знание идиоматичных паттернов:
errgroup,context.WithCancel,select. - Понимание проблемы утечки горутин и ресурсов.
- Умение обрабатывать edge cases: меньше двух успешных ответов, ошибки в горутинах, блокирующие операции.
Типичные ошибки
- Забывают вызвать
cancel()- горутины продолжат работать после выхода из цикла. - Используют небуферизованный канал - горутины заблокируются на отправке, если основная горутина уже вышла.
- Не пробрасывают контекст в HTTP-запрос - отмена не сработает для сетевых операций.
- Закрывают канал из нескольких горутин - паника при повторном закрытии.
- Не ждут завершения горутин через
wg.Wait()- возможна утечка ресурсов. - Путают
ctx.Done()иresultsвselect- если контекст отменён, но результатов ещё нет, цикл может завершиться преждевременно.
> Похожие задачи по Go
Какие проблемы могут возникнуть при возвращении внутренней мапы напрямую в Go
Как в Go встраивание заменяет наследование
Как реализовать worker pool для ограничения количества горутин в Go
Как программа узнает об окончании списка URL и передает сигнал завершения
> Похожие задачи по backend
Какие проблемы могут возникнуть при возвращении внутренней мапы напрямую в Go
Как в Go встраивание заменяет наследование
Как реализовать worker pool для ограничения количества горутин в Go
Как программа узнает об окончании списка URL и передает сигнал завершения
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью