← Назад к вопросам

Первый результат выигрывает

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()
    }
}

Как работает:

  1. Буферизированный канал на workers элементов
  2. Все горутины выполняют task и пишут результат
  3. select получает первый результат
  4. Остальные горутины продолжают выполняться (но мы их игнорируем)

Проблема: Горутины продолжают работать после возврата. Это пустая трата ресурсов и может привести к утечкам.

Улучшенная версия: Отмена остальных горутин

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 паттерн, часто используемый в микросервисах и распределённых системах.

Первый результат выигрывает | PrepBro