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 блокирует выполнение до тех пор пока горутина не будет запущена
// ошибка будет подучена в методе Wait
eGroup.Go(func() error {
data, err := getSomeThing()
if err != nil {
return err
}
resultChannel <- data
return nil
})
}
go func() {
defer close(resultChannel)
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, который помогает упростить это процесс для некоторого класса задач.