Что такое errgroup и в каких случаях его стоит использовать?

Ответ

errgroup — это пакет (golang.org/x/sync/errgroup), который предоставляет средства для синхронизации группы горутин и обработки ошибок в них. Он является расширением sync.WaitGroup со встроенной обработкой ошибок и отменой контекста.

Основные возможности

  1. Синхронизация: Метод g.Wait() блокирует выполнение до тех пор, пока все горутины, запущенные через g.Go(), не завершатся.
  2. Обработка ошибок: g.Wait() возвращает первую не-nil ошибку, полученную от любой из горутин в группе. Остальные ошибки игнорируются.
  3. Отмена через контекст: При использовании errgroup.WithContext() первая же ошибка приводит к отмене контекста. Это позволяет остальным горутинам в группе грациозно завершить свою работу, не выполняя лишних действий.
  4. Ограничение параллелизма: Метод g.SetLimit(n) позволяет ограничить количество одновременно выполняющихся горутин, что превращает errgroup в простой и эффективный пул воркеров.

Пример использования

Представим, что нужно параллельно выполнить несколько HTTP-запросов и остановиться, если хотя бы один из них завершился с ошибкой.

import (
    "context"
    "fmt"
    "net/http"
    "golang.org/x/sync/errgroup"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    urls := []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://invalid-url/", // Этот запрос вызовет ошибку
    }

    for _, url := range urls {
        url := url // https://golang.org/doc/faq#closures_and_goroutines
        g.Go(func() error {
            // При ошибке в другой горутине ctx будет отменен
            req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                fmt.Printf("Ошибка при запросе %s: %vn", url, err)
                return err // Возвращаем ошибку
            }
            resp.Body.Close()
            fmt.Printf("Успешно получен ответ от %sn", url)
            return nil
        })
    }

    // Ожидаем завершения всех горутин и проверяем ошибку
    if err := g.Wait(); err != nil {
        fmt.Printf("nОдна из операций завершилась с ошибкой: %vn", err)
    }
}

Когда использовать?

errgroup идеально подходит для сценариев, где нужно выполнить несколько независимых задач параллельно и результат всей операции считается неудачным, если хотя бы одна из подзадач провалилась. Например:

  • Параллельные запросы к разным микросервисам или базам данных.
  • Параллельная обработка набора файлов или данных.
  • Реализация Scatter-Gather паттерна.