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

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

Комментарии (4)


  1. houk
    03.01.2025 10:55

    А кто пишет в канал done? Его читают как в горутинах(подзадачах) так и из кода контролирующего запуска горутин...


    1. houk
      03.01.2025 10:55

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

      P. S. Это вкусовщина


      1. dzimitry Автор
        03.01.2025 10:55

        Да, согласен. Наверное лучший вариант ссылку на полный на код в gitlab добавить


    1. dzimitry Автор
      03.01.2025 10:55

      "... но у нас нет канала обратной связи для управления запущенными горутинами. Как минимум нужен канал done"
      - если имеется ввиду эта фразу, то про запись в done речь не шла: для остановки горутины достаточно закрыть канал done и все горутины, в которые done был передан смогут прослушать сигнал. Канал done действительно может прослушиваться родительской горутиной, но также может быть создан в родительской горутине и передан в дочерние для управления их работой(для из завершения).