Дана функция readOne(ctx, name). Один вызов выполняется быстро (около секунды),
но если звать её подряд, всё складывается. Её трогать нельзя. Дана эталонная
последовательная readAllSeq, её тоже трогать нельзя. Нужно переписать
readAllFast так, чтобы она вела себя как readAllSeq, но работала быстрее.
Сигнатуру менять нельзя.
// readOne быстро выполняется при одиночном вызове,
// но медленно, если звать подряд. Считаем, что
// внутри неё оптимизировать нечего.
func readOne(ctx context.Context, name string) ([]byte, error) {
if name == "" {
return nil, fmt.Errorf("invalid name %q", name)
}
tick := time.NewTicker(time.Second)
defer tick.Stop()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-tick.C:
}
if strings.HasPrefix(name, "invalid") {
return nil, fmt.Errorf("invalid name %q", name)
}
buf := make([]byte, 10)
n, e := rand.Read(buf)
if e != nil {
return nil, fmt.Errorf("getting file %q: %w", name, e)
}
return buf[:n], nil
}
// readAllSeq — эталон для сравнения поведения, не трогаем.
func readAllSeq(ctx context.Context, names ...string) (out map[string][]byte, err error) {
if len(names) == 0 {
return nil, nil
}
out = make(map[string][]byte, len(names))
for _, name := range names {
out[name], err = readOne(ctx, name)
if err != nil {
return nil, err
}
}
return out, nil
}
// readAllFast можно менять, кроме сигнатуры.
func readAllFast(ctx context.Context, names ...string) (out map[string][]byte, err error)
readOne и readAllSeq уже лежат в пакете. Объявлять их заново не нужно, пиши
только readAllFast. В проверке readOne подменяется фейком с другими
задержками, поэтому вызывай именно её.
Требования
- Результат как у
readAllSeq. Пустой список имён — nil, nil. Все чтения
успешны — карта имя → данные по каждому имени. Хотя бы одно упало —
nil и ошибка этого чтения, а не context.Canceled от соседей, которых
отменили.
- Все
readOne идут параллельно: N имён читаются примерно за время одного
чтения, а не за N.
- На первой ошибке остальные чтения отменяются: контекст, переданный в
readOne, отменён, и функция не ждёт, пока они досидят до конца.
- Если отменили внешний
ctx, функция возвращает nil и ошибку, для которой
errors.Is(err, context.Canceled).
- После возврата горутин не остаётся. Код проходит
go test -race.
Пример
start := time.Now()
files, err := readAllFast(context.TODO(), "1", "2", "3")
fmt.Println(time.Since(start), len(files), err) // ≈1s 3 <nil>, а readAllSeq — ≈3s
_, err = readAllFast(context.TODO(), "1", "invalid-2", "3")
fmt.Println(err) // invalid name "invalid-2"
Устно: что даёт errgroup.WithContext и чем он отличается от
WaitGroup + канал ошибок? Как ограничить число одновременных чтений, если имён
десять тысяч?