sync/errgroup для Go: полное руководство с примерами
1. Что это и зачем нужна
Библиотека sync/errgroup — это часть официального расширения golang.org/x/sync, предоставляющая мощный и элегантный способ управления параллельными горутинами с обработкой ошибок. В стандартной библиотеке Go есть пакет sync с WaitGroup, но он не умеет собирать ошибки от горутин. errgroup решает эту проблему: вы запускаете несколько горутин, и если хотя бы одна из них возвращает ошибку, группа немедленно (или по завершению всех) сигнализирует об этом, отменяя контекст и позволяя остальным горутинам корректно завершиться.
Зачем это нужно? В реальных приложениях часто требуется выполнить несколько независимых задач параллельно: загрузить файлы, обработать данные, сделать запросы к API. Если одна задача упадёт с ошибкой, нет смысла продолжать остальные — нужно быстро остановиться и вернуть ошибку пользователю. errgroup делает это автоматически: вы передаёте контекст, и при первой ошибке контекст отменяется, а все горутины, которые слушают этот контекст, могут завершиться досрочно.
Это особенно полезно в микросервисах, веб-серверах и CLI-утилитах, где важна скорость реакции на ошибки. В отличие от ручного управления с каналами и select, errgroup даёт чистый, декларативный API, который сложно использовать неправильно.
2. Установка
sync/errgroup не входит в стандартную библиотеку Go, но является частью подмодуля golang.org/x/sync. Установка выполняется одной командой:
go get golang.org/x/sync/errgroup
После этого вы можете импортировать пакет в свой код:
import "golang.org/x/sync/errgroup"
Убедитесь, что у вас установлен Go версии 1.13 или выше (рекомендуется 1.18+ для поддержки generics, хотя errgroup их не использует). Если вы используете модули (go.mod), команда go get автоматически обновит файл go.sum.
3. Быстрый старт — минимальный рабочий пример
Ниже приведён простейший пример, который запускает три горутины, каждая из которых возвращает ошибку, если переданное число отрицательное. Если хотя бы одна горутина вернёт ошибку, errgroup.Wait() вернёт её.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
numbers := []int{1, -2, 3}
for _, n := range numbers {
n := n // захват переменной
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if n < 0 {
return fmt.Errorf("negative number: %d", n)
}
fmt.Printf("Processed %d\n", n)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All tasks completed successfully")
}
}
Вывод: Processed 1, Processed 3, Error: negative number: -2. Обратите внимание, что горутина с числом 3 могла не выполниться, если бы контекст был отменён до её запуска — это демонстрирует механизм ранней остановки.
4. Основные методы/функции
Ниже приведены все ключевые элементы API пакета errgroup. Каждый метод сопровождается сигнатурой, описанием и рабочим примером.
4.1. WithContext
Сигнатура: func WithContext(ctx context.Context) (*Group, context.Context)
Что делает: Создаёт новую группу Group и производный контекст, который будет отменён при первой ошибке, возвращённой любой горутиной. Возвращает указатель на группу и новый контекст.
ctx := context.Background()
g, ctx := errgroup.WithContext(ctx)
// Теперь ctx будет отменён, если любая горутина вернёт ошибку
g.Go(func() error {
return errors.New("something went wrong")
})
err := g.Wait() // err != nil
fmt.Println(ctx.Err()) // context.Canceled
4.2. Go
Сигнатура: func (g *Group) Go(f func() error)
Что делает: Запускает переданную функцию f в новой горутине. Если функция возвращает ошибку, группа запоминает её (первую) и отменяет контекст. Вызов Go не блокируется.
g, _ := errgroup.WithContext(context.Background())
g.Go(func() error {
// Имитация работы
time.Sleep(100 * time.Millisecond)
return nil
})
g.Go(func() error {
return errors.New("fail")
})
err := g.Wait() // err.Error() == "fail"
4.3. Wait
Сигнатура: func (g *Group) Wait() error
Что делает: Блокирует выполнение до тех пор, пока все горутины, запущенные через Go, не завершатся. Возвращает первую ошибку (если она была), иначе nil. Вызов Wait можно выполнять только один раз; повторный вызов приведёт к панике.
g, _ := errgroup.WithContext(context.Background())
g.Go(func() error { return nil })
g.Go(func() error { return nil })
if err := g.Wait(); err != nil {
log.Fatal(err)
}
fmt.Println("All done")
4.4. SetLimit
Сигнатура: func (g *Group) SetLimit(n int)
Что делает: Ограничивает количество одновременно работающих горутин. Если n меньше или равно 0, лимит снимается. По умолчанию лимит не установлен (неограниченное количество). Полезно для контроля нагрузки.
g, _ := errgroup.WithContext(context.Background())
g.SetLimit(2) // максимум 2 горутины одновременно
for i := 0; i < 10; i++ {
i := i
g.Go(func() error {
fmt.Printf("Working on %d\n", i)
time.Sleep(100 * time.Millisecond)
return nil
})
}
g.Wait()
4.5. TryGo
Сигнатура: func (g *Group) TryGo(f func() error) bool
Что делает: Пытается запустить горутину, но если лимит (установленный через SetLimit) исчерпан, возвращает false и не запускает функцию. Если лимит не установлен, всегда возвращает true. Позволяет реализовать неблокирующий запуск.
g, _ := errgroup.WithContext(context.Background())
g.SetLimit(1)
ok := g.TryGo(func() error {
time.Sleep(50 * time.Millisecond)
return nil
})
fmt.Println(ok) // true
ok = g.TryGo(func() error {
return nil
})
fmt.Println(ok) // false, так как лимит 1 и первая горутина ещё работает
4.6. Go с контекстом (неявно)
Сигнатура: Нет отдельного метода, но контекст, возвращённый WithContext, используется внутри Go. Рекомендуется проверять ctx.Done() внутри горутины.
Что делает: Позволяет горутинам реагировать на отмену контекста. Это не отдельный метод, а паттерн использования.
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(1 * time.Second):
return nil
}
})
4.7. Go с паникой (recover)
Сигнатура: Нет отдельного метода. errgroup автоматически восстанавливает панику в горутине и возвращает её как ошибку.
Что делает: Если горутина паникует, errgroup перехватывает панику и возвращает её как ошибку через Wait. Это повышает надёжность.
g, _ := errgroup.WithContext(context.Background())
g.Go(func() error {
panic("unexpected error")
})
err := g.Wait()
fmt.Println(err) // "unexpected error"
4.8. Go с замыканием (захват переменной)
Сигнатура: Нет отдельного метода. Важно правильно захватывать переменные в цикле.
Что делает: Предотвращает гонку данных при использовании переменных цикла внутри горутины.
g, _ := errgroup.WithContext(context.Background())
for i := 0; i < 5; i++ {
i := i // захват
g.Go(func() error {
fmt.Println(i)
return nil
})
}
g.Wait()
5. Реальный практический пример (100-200 строк кода)
Рассмотрим сценарий: веб-сканер, который проверяет доступность нескольких URL-адресов. Если хотя бы один URL недоступен, мы хотим немедленно остановиться и вернуть ошибку. Используем errgroup с ограничением параллелизма.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
"time"
)
func checkURL(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("URL %s returned status %d", url, resp.StatusCode)
}
return nil
}
func main() {
urls := []string{
"https://google.com",
"https://github.com",
"https://nonexistent.example.com",
"https://pythonlib.ru",
}
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(3) // не более 3 одновременных запросов
for _, url := range urls {
url := url
g.Go(func() error {
// Проверяем, не отменён ли контекст
select {
case <-ctx.Done():
return ctx.Err()
default:
}
fmt.Printf("Checking %s...\n", url)
err := checkURL(ctx, url)
if err != nil {
return fmt.Errorf("failed to check %s: %w", url, err)
}
fmt.Printf("OK: %s\n", url)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All URLs are reachable")
}
}
В этом примере:
— SetLimit(3) ограничивает параллельные запросы.
— Контекст отменяется при первой ошибке, и остальные горутины (если они ещё не запущены) завершаются через ctx.Done().
— Ошибка оборачивается с помощью fmt.Errorf для сохранения контекста.
— Если бы мы не использовали errgroup, пришлось бы вручную управлять каналами и select.
6. Советы по производительности и типичные ошибки
Производительность
- Используйте
SetLimitдля контроля параллелизма. Без лимитаerrgroupможет запустить тысячи горутин, что приведёт к исчерпанию памяти и дескрипторов. Установите разумный лимит, например,runtime.GOMAXPROCS(0) * 2. - Избегайте дорогих операций внутри горутин. Если задача требует много CPU, используйте пул воркеров (например, через каналы) вместо
errgroup— он не оптимизирован для CPU-bound задач. - Не создавайте
errgroupна каждый запрос. Если вы обрабатываете много запросов, создавайте одну группу на пакет задач, а не на каждую микро-операцию. - Проверяйте контекст в цикле. Если горутина выполняет итеративный процесс (например, чтение строк из файла), проверяйте
ctx.Done()на каждой итерации, чтобы быстро реагировать на отмену.
Типи
Также в библиотеке
JWT токены в Swift: полное руководство по работе с JSON Web Tokens
Работа с CSV в Swift (iOS/macOS): библиотека SwiftCSV и основы парсинга
Firebase SDK для Swift: полное руководство с примерами
Реактивное программирование в Go (Golang): библиотеки, паттерны и примеры
Криптография в Node.js: шифрование и хеширование с crypto