Код-ревью Senior Паттерны конкурентности спрашивают часто обновлено

Код-ревью Go: параллельная обработка с ошибками

Вопрос

Функция обрабатывает идентификаторы параллельно и возвращает результаты или первую ошибку. Найдите проблемы.

func ProcessAll(ctx context.Context, ids []int) ([]Result, error) {
    results := make(chan Result)
    errc := make(chan error)
    var wg sync.WaitGroup
    for _, id := range ids {
        wg.Add(1)
        go func() {
            defer wg.Done()
            r, err := process(ctx, id)
            if err != nil {
                errc <- err
                return
            }
            results <- r
        }()
    }
    wg.Wait()
    close(results)
    var out []Result
    for r := range results {
        out = append(out, r)
    }
    select {
    case err := <-errc:
        return nil, err
    default:
    }
    return out, nil
}

Коротко

Всё это решает errgroup: Лимит параллелизма, отмена по первой ошибке, порядок по индексу, никаких каналов, которые можно забыть прочитать.

Что должен найти ревьюер

  1. Производительностьстрока 5
    Горутина на каждый id без ограничения: на большом входе это перегрузит базу или внешний сервис. Нужен лимит (SetLimit, семафор, пул).
  2. Ошибкастрока 17
    Дедлок: горутины блокируются на отправке в небуферизованный results, потому что читать начнут только после wg.Wait(), а Wait ждёт эти же горутины.
  3. Ошибкастрока 11
    Небуферизованный errc: читают из него максимум один раз и только неблокирующе, поэтому горутины с ошибками повиснут навсегда. Утечка горутин.
  4. Производительностьстрока 9
    После первой ошибки остальные задачи продолжают работать: нет отмены общего контекста.
  5. Ошибкастрока 21
    Результаты собираются в порядке завершения, а не в порядке ids: вызывающий не сможет сопоставить результат и id.

Разбор

Всё это решает errgroup:

func ProcessAll(ctx context.Context, ids []int) ([]Result, error) {
    out := make([]Result, len(ids))
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(16)
    for i, id := range ids {
        g.Go(func() error {
            r, err := process(ctx, id)
            if err != nil {
                return fmt.Errorf("process %d: %w", id, err)
            }
            out[i] = r
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return out, nil
}

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

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.