Что такое errgroup в Go и какую проблему он решает?

Ответ

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

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

Ключевые возможности:

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

Пример: параллельное выполнение HTTP-запросов

func fetchURLs(ctx context.Context, urls []string) error {
    g, gCtx := errgroup.WithContext(ctx)

    for _, url := range urls {
        // Захватываем переменную url в замыкании
        url := url 
        g.Go(func() error {
            req, err := http.NewRequestWithContext(gCtx, http.MethodGet, url, nil)
            if err != nil {
                return err
            }
            resp, err := http.DefaultClient.Do(req)
            // Если gCtx был отменен (из-за ошибки в другой горутине),
            // http.Client вернет ошибку, и эта горутина завершится.
            if err != nil {
                return err
            }
            resp.Body.Close()
            fmt.Printf("Fetched %s with status %sn", url, resp.Status)
            return nil
        })
    }

    // Ждем завершения всех горутин и возвращаем первую ошибку, если она была.
    return g.Wait()
}

errgroup является более мощной и удобной альтернативой sync.WaitGroup, когда требуется не просто дождаться завершения горутин, но и управлять их отменой и обрабатывать ошибки.

Ответ 18+ 🔞

А, слушай, про errgroup! Это же та самая штука, которая превращает твой код из "запустил десять горутин и молишься, чтобы они все сами как-то завершились" в "взрослый, управляемый бардак".

Представь себе: ты на кухне, готовишь обед. Кастрюли на всех конфорках, духовка греется, в микроволновке что-то пищит. И вот одна кастрюля, сука, убегает. По старинке ты бы бегал, выключал всё по отдельности, ругался. А errgroup — это как умная система, которая видит убегающую кастрюлю и тут же вырубает всю плиту, духовку и даже микроволновку. "Всё, пиздец, процесс пошёл по пизде, дальше готовить бессмысленно".

Короче, что он умеет, этот красавец:

  1. Запустить кучу дел параллельно — как sync.WaitGroup.
  2. Поймать первую же ошибку и выдать её тебе, как главную залупу.
  3. Самое сокровенное: если ты создал его через WithContext, то при первой же ошибке он автоматом отменяет контекст для всех остальных горутин. Они получают сигнал "ребята, всё, пиздуй, дальше работать не надо" и могут грациозно завершиться, а не тупо висеть.

Вот смотри, как это выглядит в деле (запросы куда-то там):

func fetchURLs(ctx context.Context, urls []string) error {
    // Создаём группу с привязанным контекстом. Это ключ!
    g, gCtx := errgroup.WithContext(ctx)

    for _, url := range urls {
        url := url // Важный момент, чтоб в замыкании не наебаться
        g.Go(func() error {
            // ВНИМАНИЕ! Используем gCtx, а не оригинальный ctx.
            // Если в другой горутине пиздец, этот контекст отменится,
            // и http.Client просто не станет делать запрос или оборвёт его.
            req, err := http.NewRequestWithContext(gCtx, http.MethodGet, url, nil)
            if err != nil {
                return err // Вернул ошибку -> триггер на отмену для всех!
            }
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err
            }
            resp.Body.Close()
            fmt.Printf("Успешно сходили на %s, статус: %sn", url, resp.Status)
            return nil
        })
    }

    // Ждём. Все завершились? Отлично. Кто-то накосячил? Получи первую ошибку.
    return g.Wait()
}

Вот и вся магия. Без этого пришлось бы городить каналы, select-ы, sync.WaitGroup и следить, кто кого отменил. А тут — одна строчка errgroup.WithContext, и ты уже почти архитектор распределённых систем, блядь.

Короче, если делаешь что-то параллельное и там может что-то пойти не так — бери errgroup. Он сэкономит тебе кучу нервов и строк кода. А нервы, они, нахуй, не восстанавливаются.