Две библиотеки решают разные задачи. errgroup управляет группой задач и их отменой. singleflight объединяет одинаковые вызовы в один, пока исходная операция не завершилась.

errgroup: группа с общей отменой

g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
    g.Go(func() error {
        return fetch(ctx, url)
    })
}
if err := g.Wait(); err != nil {
    return err
}

При первой ошибке контекст группы отменяется. Wait ждёт завершения всех горутин и возвращает первую ошибку. Он не убивает работу: функция fetch сама должна реагировать на ctx.Done().

Частая ошибка - запуск без контекста. Тогда ошибка возвращается, но остальные операции продолжают идти.

Ограничение параллелизма

SetLimit(n) ограничивает одновременно работающие задачи. Go блокируется, пока место не освободится, и контекст этого ожидания не прерывает. Если отмена нужна уже при постановке задачи, используют TryGo и обрабатывают false, либо сначала ограничивают список работ, а затем запускают их обычным Go.

Для HTTP-запросов это ограничивает число соединений и защищает downstream. Для CPU-вычислений лимит обычно ближе к GOMAXPROCS, но выбирается после профилирования.

singleflight: дедупликация работы

Когда сто клиентов одновременно просят один товар, нет смысла сто раз идти в базу:

var group singleflight.Group

value, err, shared := group.Do("product:"+id, func() (any, error) {
    return loadProduct(id)
})

Один вызов выполняет работу, остальные ждут и получают тот же результат. shared == true означает, что значение разделяется.

singleflight запрос A запрос B запрос C один вызов БД

Одновременные запросы с одним ключом ждут одну работу и получают общий результат. После завершения singleflight ничего не кэширует.

Это не кэш: после завершения запись исчезает. Для постоянного кэша нужен отдельный компонент с TTL и ограничением размера.

Ошибки и мутации

Все участники получают одни и те же данные. Если результат срез или map, нельзя менять его независимо: возникнет гонка. Возвращают иммутабельное значение, копию или делают копирование при выдаче.

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

DoChan позволяет ждать результат через канал и уметь выйти по контексту. Сама функция всё равно должна поддерживать отмену, иначе ожидание внешней работы останется.

Коротко

  • errgroup отменяет группу по первой ошибке и ждёт всех через Wait.
  • SetLimit ограничивает параллелизм, но не заменяет отмену.
  • singleflight дедуплицирует работу только до её завершения.
  • Разделяемый результат нельзя мутировать: делают копию или иммутабельное значение.