> Как остановить обработку после получения первых двух успешных запросов в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

Используйте errgroup с контекстом или канал с буфером и sync.Once. Самый идиоматичный способ - errgroup.WithContext(ctx), запустить N горутин, каждая отправляет результат в канал, а основная горутина читает ровно два значения и вызывает cancel(). Альтернатива - context.WithCancel вручную и select в каждой горутине. Главное - гарантировать, что после второго успеха все остальные горутины завершатся, а ресурсы освободятся.

Подробное объяснение

Задача сводится к координации конкурентных операций с ранним выходом. В Go есть несколько подходов:

  1. errgroup + context - самый чистый вариант. errgroup.WithContext создаёт контекст, который отменяется при первой ошибке или вручную через cancel(). Каждая горутина проверяет ctx.Done() перед отправкой результата.

  2. Канал с буфером + sync.Once - если нужно собрать ровно N результатов, а не отменить всё. Канал с буфером размера 2 позволяет горутинам не блокироваться, а sync.Once гарантирует, что закрытие канала произойдёт один раз.

  3. context.WithCancel вручную - даёт больше контроля, но требует аккуратной работы с defer cancel() и проверкой ctx.Err().

Ключевой trade-off: errgroup удобен, когда нужно дождаться завершения всех горутин, но здесь мы хотим выйти раньше. Поэтому основная горутина читает из канала, а после второго значения вызывает cancel(). Остальные горутины при следующей проверке контекста завершатся.

Важно: если горутины выполняют блокирующие операции (например, HTTP-запросы), отмена контекста должна пробрасываться в сам запрос через http.NewRequestWithContext. Иначе горутина не завершится, пока операция не закончится естественным путём.

На практике

Типичный сценарий - параллельный опрос нескольких сервисов, где нужны первые два успешных ответа. Например, поиск по нескольким источникам: как только два источника ответили, остальные можно отменить.

Паттерн выглядит так:

  • Создаём ctx, cancel := context.WithCancel(context.Background()).
  • Запускаем горутины, каждая делает запрос с этим контекстом.
  • Результаты отправляем в канал results.
  • В основной горутине читаем из канала в цикле, считаем успешные ответы.
  • После второго успеха вызываем cancel() и выходим из цикла.

Важно обрабатывать случай, когда горутин меньше двух - тогда цикл завершится по закрытию канала или по ctx.Done().

Пример кода

GO
package main
import (
"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.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fetch(ctx, u, results)
}(url)
}
var successes []string
for 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 - если контекст отменён, но результатов ещё нет, цикл может завершиться преждевременно.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью