ErrorHandling-патерн в golang

от автора

Обработка и передача ошибок в конкурентном коде имеет некоторые особенности. Поскольку решении о запуске подзадачи(или подзадач) принимается вне запущенной горутины, центр обработки информации(в данном случае ошибки) должен находится в другом месте. Это может быть часть кода инициирующая запуск горутин(родительская горутина) и ожидающая результатов ее выполнения или отдельная горутина, запущенная для этих целей.

Будем отталкиваться от примера, где мы ожидаем результатов выполнения 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *