Расскажите про пакет `golang.org/x/sync/errgroup` и ваш опыт его использования

Ответ

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

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

Основные сценарии использования:

  • Fan-out: Параллельное выполнение однотипных независимых операций (например, запросы к нескольким внешним API).
  • Graceful Shutdown: Координированное завершение нескольких компонентов сервиса (например, остановка HTTP-сервера и закрытие соединения с БД).
  • Ограничение параллелизма: С помощью метода SetLimit(n) можно ограничить количество одновременно работающих горутин, что полезно для контроля потребления ресурсов.

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

Представим, что нам нужно параллельно получить данные о пользователе и его заказах из разных сервисов.

func GetUserAndOrders(ctx context.Context, userID string) (*User, *Orders, error) {
    g, ctx := errgroup.WithContext(ctx)

    var user *User
    g.Go(func() error {
        // Важно передавать ctx внутрь, чтобы сработала отмена
        var err error
        user, err = fetchUser(ctx, userID)
        return err // Возвращаем ошибку, если она есть
    })

    var orders *Orders
    g.Go(func() error {
        // Проверяем ctx.Done() для быстрой отмены
        select {
        case <-ctx.Done():
            return ctx.Err() // Возвращаем ошибку отмены
        default:
        }

        var err error
        orders, err = fetchOrders(ctx, userID)
        return err
    })

    // Ожидаем завершения обеих горутин.
    // Если одна из них вернет ошибку, Wait() вернет ее, а контекст будет отменен.
    if err := g.Wait(); err != nil {
        return nil, nil, err
    }

    return user, orders, nil
}

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