Обработка и передача ошибок в конкурентном коде имеет некоторые особенности. Поскольку решении о запуске подзадачи(или подзадач) принимается вне запущенной горутины, центр обработки информации(в данном случае ошибки) должен находится в другом месте. Это может быть часть кода инициирующая запуск горутин(родительская горутина) и ожидающая результатов ее выполнения или отдельная горутина, запущенная для этих целей.
Будем отталкиваться от примера, где мы ожидаем результатов выполнения n-горутин. Результат читаем из канала resultChannel:
workerNumber := 5 resultChannel := make(chan Result) for i := 0; i < workerNumber; i++ { go func() { var result Result defer func() { resultChannel <- result }() data, err := getSomeThing() if err != nil { result.Error = err return } result.Data = data }() } for i := 0; i < workerNumber; i++ { result := <-resultChannel if result.Error != nil { fmt.Printf("Ошибка: %v\n", result.Error) continue } }
Мы отправляем информацию в канал resultChannel, где помимо поля с результатом выполнения(Data), есть поле(Error), куда мы поместим информацию об ошибке в случае ее возникновения:
type Result struct { Error error Data interface{} }
Этот код не готов к использованию. Мы получим данные от горутины, но у нас нет канала обратной связи для управления запущенными горутинами. Как минимум нужен канал done. Освежить знания о канале Done и работе с контекстом можно здесь.
Код горутины:
... select { case <-done: return default: data, err := getSomeThing() if err != nil { result.Error = err return } result.Data = data } ...
Обработка результатов работы горутины тоже изменится:
for i := 0; i < workerNumber; i++ { select { case <-done: break default: result := <-resultChannel if result.Error != nil { fmt.Printf("Ошибка: %v\n", result.Error) continue } } }
Помимо этого код может усложнится различными бизнес сценариями в том числе связанными с обработками ошибок. Например, когда результат работы отдельной подзадачи(горутины) оказывает влияние на логику выполнения программы: в случае разбиения задачи на подзадачи, при котором не успешный результат выполнения хотя бы одной подзадачи лишает смысл выполнения других: например, сложный sql запрос разбит на подзапросы. Для выполнения каждого подзапроса запускаем горутину и в случае возникновения ошибки хотя бы в одной из подзадач нет смысла дожидаться выполнения остальных.
То есть требования могут быть сформулированы таким образом: при получении ошибки выполнения от одной из горутин группы нам требуется остановить выполнение остальных. Для этого мы можем завести еще один канал: cancelChannel, который будут слушать группа горутин и останавливать выполнение в случае получения сигнала из него.
Код горутины
... select { case <-done: return case <-cancelChannel: return default: data, err := getSomeThing() if err != nil { result.Error = err return } result.Data = data } ...
Код обработчика ошибок:
result := <-resultChannel if result.Error != nil { fmt.Printf("Ошибка: %v\n", result.Error) close(cancelChannel) break }
Код становится сложнее для понимания. Пакет errgroup помогает решить эту проблему для описанного класса задач:
resultChannel := make(chan interface{}, workerNumber) // создаем группу для работы с горутинами eGroup := errgroup.Group{} // устанавливаем лимит на кол-во одновременно запущенных горутин в группе eGroup.SetLimit(workerNumber) for i := 0; i < workerNumber; i++ { //запускает функцию в отдельной горутине //если к-во горутин превышает установленный limit; //метод Go блокирует выполнение до тех пор пока горутина не будет запущена eGroup.Go(func() error { data, err := getSomeThing() if err != nil { return err } resultChannel <- data return nil }) } go func() { defer close(resultChannel) //метод Wait блокирует до тех пор пока все функции, //указанные в методе Go не вернут значения; //возращает первую не нулевую ошибку, возвращенную функцией, //если такая случится if err := eGroup.Wait(); err != nil { fmt.Printf("Ошибка: %v\n", err) } }() for result := range resultChannel { fmt.Println(result) } fmt.Println("Все горутины завершили работу")
Говоря о пакете errorgroup также стоит упоминуть о двух методах:
func TryGo(f func() error) bool — запускает функцию в новой горутине, если количество запущенных горутин в группе меньше установленного лимита. Возвращаемое значение содержит результат запуска: запущена горутина или нет.
func WithContext(ctx context.Context) (*Group, context.Context) — производный контекст отменяется как только переданная в метод Go функция возвращает не нулевую ошибку или когда Wait вернет значение первый раз в зависимости от того, что произойдет раньше. Поскольку наши горутины могут порождать другие горутины последняя функция может быть очень полезной.
Заключение
Рассмотрен подход к обработке ошибок в golang, а также пакет errorgroup, который помогает упростить это процесс для некоторого класса задач.
ссылка на оригинал статьи https://habr.com/ru/articles/871394/
Добавить комментарий