← Назад к вопросам
Первый результат выигрывает
2.0 Middle🔥 171 комментариев
#Основы Go
Условие
Реализуйте паттерн "первый результат выигрывает": запустите несколько горутин, которые выполняют одну и ту же задачу, и верните результат первой завершившейся.
Сигнатура
func firstWins(ctx context.Context, workers int, task func() (int, error)) (int, error)
Требования
- Запустить workers горутин, каждая выполняет task
- Вернуть результат первой успешно завершившейся горутины
- Поддержать отмену через context
- Корректно остановить оставшиеся горутины после получения первого результата
Пример использования
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := firstWins(ctx, 3, func() (int, error) {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
return 42, nil
})
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Первый результат выигрывает
Основная идея
Паттерн "first wins" часто используется в production для:
- Гонки между несколькими источниками данных (кеши, БД, API)
- Ускорение обработки: берём результат первого
- Failover: если одно не работает, другое сработает
Используем канал результатов и select для получения первого значения.
Простая реализация
import (
"context"
"fmt"
)
type result struct {
value int
err error
}
func firstWins(ctx context.Context, workers int, task func() (int, error)) (int, error) {
// Канал для результатов: буферизирован на случай, если все завершатся до проверки
resultCh := make(chan result, workers)
// Запускаем горутины
for i := 0; i < workers; i++ {
go func(id int) {
// Выполняем задачу
val, err := task()
// Отправляем результат (неблокирующе благодаря буферу)
resultCh <- result{value: val, err: err}
}(i)
}
// Ждём первого результата
select {
case res := <-resultCh:
return res.value, res.err
case <-ctx.Done():
return 0, ctx.Err()
}
}
Как работает:
- Буферизированный канал на
workersэлементов - Все горутины выполняют task и пишут результат
selectполучает первый результат- Остальные горутины продолжают выполняться (но мы их игнорируем)
Проблема: Горутины продолжают работать после возврата. Это пустая трата ресурсов и может привести к утечкам.
Улучшенная версия: Отмена остальных горутин
func firstWins(ctx context.Context, workers int, task func() (int, error)) (int, error) {
// Контекст для отмены остальных горутин
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
resultCh := make(chan result, 1) // Буфер на 1 для первого результата
// Запускаем горутины
for i := 0; i < workers; i++ {
go func(id int) {
// Проверяем, не отменён ли контекст
select {
case <-cancelCtx.Done():
return
default:
}
// Выполняем задачу
val, err := task()
// Пытаемся отправить результат
select {
case resultCh <- result{value: val, err: err}:
// Успешно отправили
case <-cancelCtx.Done():
// Отмена произошла во время отправки
return
}
}(i)
}
// Ждём первого результата
select {
case res := <-resultCh:
cancel() // Отменяем остальные горутины
return res.value, res.err
case <-ctx.Done():
cancel()
return 0, ctx.Err()
}
}
Улучшения:
cancel()отменяет контекст для всех горутин- Горутины проверяют
cancelCtx.Done()и завершаются - Буфер на 1, а не на все workers (экономит память)
Версия с WaitGroup для полной синхронизации
import "sync"
func firstWins(ctx context.Context, workers int, task func() (int, error)) (int, error) {
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
resultCh := make(chan result, 1)
var wg sync.WaitGroup
// Запускаем горутины
for i := 0; i < workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Проверяем отмену
select {
case <-cancelCtx.Done():
return
default:
}
// Выполняем задачу
val, err := task()
// Отправляем результат
select {
case resultCh <- result{value: val, err: err}:
// Отправили первый результат
case <-cancelCtx.Done():
// Отмена
}
}(i)
}
// Горутина для закрытия канала когда все завершатся
go func() {
wg.Wait()
close(resultCh)
}()
// Получаем результат
select {
case res, ok := <-resultCh:
if ok {
return res.value, res.err
}
return 0, fmt.Errorf("no results")
case <-ctx.Done():
return 0, ctx.Err()
}
}
Версия с timeout на отдельную задачу
func firstWinsWithTaskTimeout(ctx context.Context, workers int, taskTimeout time.Duration, task func(context.Context) (int, error)) (int, error) {
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
resultCh := make(chan result, 1)
for i := 0; i < workers; i++ {
go func(id int) {
select {
case <-cancelCtx.Done():
return
default:
}
// Создаём контекст с timeout для каждой задачи
taskCtx, taskCancel := context.WithTimeout(cancelCtx, taskTimeout)
val, err := task(taskCtx)
taskCancel()
select {
case resultCh <- result{value: val, err: err}:
case <-cancelCtx.Done():
}
}(i)
}
select {
case res := <-resultCh:
cancel()
return res.value, res.err
case <-ctx.Done():
cancel()
return 0, ctx.Err()
}
}
Пример использования и результаты
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := firstWins(ctx, 3, func() (int, error) {
delay := time.Duration(rand.Intn(1000)) * time.Millisecond
time.Sleep(delay)
if rand.Intn(2) == 0 {
return 42, nil
}
return 0, fmt.Errorf("worker failed")
})
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Winner result: %d\n", result)
}
}
Когда это полезно
- Чтение из нескольких кешей: первый, кто вернул данные
- Несколько API endpoints: берём ответ от первого быстрого
- Зеркала серверов: подключаемся к первому доступному
- Redundancy: если один сервис падает, другой берёт на себя
- Race conditions: гарантированно получаем результат от кого-то
Ключевые моменты
- Контекст отмены:
cancel()останавливает остальные горутины - Буфер канала: важен для избежания deadlock
- Select на отправку: проверяем
Done()во время отправки - Иди: Go рекомендует иметь mechanism для остановки горутин
- WaitGroup: гарантирует завершение всех горутин перед выходом
Это production-ready паттерн, часто используемый в микросервисах и распределённых системах.