Вы можете найти весь код, упомянутый в этой статье, и другие примеры в Github Repository.

Мощной функцией языка программирования Go является возможность создавать и управлять облегченными параллельными потоками, известными как Go-Routines. В этой статье мы рассмотрим, как поддерживать их работоспособность в течение длительного времени, изящно восстанавливаясь после паники. Сначала мы продемонстрируем пример, который показывает, как паника может возникнуть в горутине, а затем перейдем к демонстрации того, как эффективно восстановить и перезапустить ее, когда такая паника произойдет. Поняв эти концепции, вы будете лучше подготовлены к разработке и внедрению надежных, отказоустойчивых приложений с использованием функций параллелизма Go.

Давайте посмотрим на первый пример. В этом примере мы рассмотрим пример паники, происходящей в подпрограмме go.

func main() {
   worker_one := workers.NewWorker("worker_1", 2*time.Second)

   go worker_one.Work()

   select {} // block forever
}

Здесь создается новый экземпляр Worker. Конструктор принимает два аргумента: строковый идентификатор («worker_1»), чтобы дать рабочему процессу имя, и продолжительность (в данном случае 2 секунды), чтобы имитировать время, необходимое для выполнения работы. Затем мы вызываем метод Work() экземпляра worker_one в новой горутине. Ожидается, что метод Work() выполнит некоторый объем работы и будет выполняться одновременно, не блокируя основную функцию. Чтобы основная функция не существовала, мы используем пустой select. заявление. Эта строка блокирует основную функцию на неопределенный срок. Это делается для того, чтобы программа продолжала работать, чтобы worker_one Go-Routine могла продолжать выполнять свои задачи в фоновом режиме. Если бы эта строка не была включена, основная функция вышла бы немедленно, и Go-Routine была бы завершена.

Теперь рассмотрим рабочий пакет.

В этом пакете мы видим пользовательский тип Worker со следующими полями.

  • ID: строковый идентификатор исполнителя.
  • Err: поле ошибки для хранения любой ошибки, которая может возникнуть во время выполнения воркера.
  • Duration: time.Duration, представляющий продолжительность, которую мы будем использовать для имитации времени, необходимого для выполнения работы.

Есть несколько методов для этого типа `Worker`.

  • Метод func (w *Worker) GetError() error возвращает ошибку, хранящуюся в поле `Err` исполнителя.
  • Строка func (w *Worker) GetWorkerID() возвращает идентификатор рабочего процесса.
  • func (w *Worker) GetSleepDuration() time.Duration возвращает продолжительность сна рабочего процесса.
  • func (w *Worker) Work() (err error) отвечает за выполнение задачи работника. Он состоит из следующих шагов:
    - Вывести сообщение о том, что рабочий процесс запущен.
    - Войти в бесконечный цикл, который выполняет следующие действия:
    1. Заполнить генератор случайных чисел текущим время.
    2. Сгенерировать случайное большое целое число в диапазоне от 0 до 99.
    3. Вывести сообщение о том, что работник выполняет работу.
    4. Проверить, является ли сгенерированное число простое число. Если это так, поднимите панику информативным сообщением.
    5. Приостановите работу на указанное время, прежде чем начинать следующую итерацию.

Теперь давайте запустим это приложение и посмотрим, что произойдет.

Starting Worker : worker_1 

worker_1 doing work ..

worker_1 doing work ..

panic: random 23 is prime 


goroutine 6 [running]:
go-routine-panic-recover/panic_example/workers.(*Worker).Work(0x14000070060)
        /Users/username/dev/go-routine-panic-recover/panic_example/workers/hard_worker.go:44 +0x234
created by main.main
        /Users/username/dev/go-routine-panic-recover/panic_example/app.go:12 +0x78
exit status 2

Этот результат демонстрирует то, что мы ожидали. Когда вызывается Work, он генерирует 3 числа и вызывает панику, когда третье число является простым. Поскольку паника не обработана, она завершает программу.

Теперь давайте посмотрим, как изменить реализацию, чтобы восстановиться в случае паники и сохранить программу в рабочем состоянии.

Давайте посмотрим, как изменилась основная функция.

worker_one := workers.NewWorker("worker_1", 2*time.Second)

wrks := make(chan *workers.Worker, 1)

go worker_one.Work(wrks)

for w := range wrks {

   fmt.Printf("\033[31m---------------- PANIC happened in worker : \033[0m\033[34m%s\033[0m\033[31m because %s\033[0m\n", w.GetWorkerID(), w.GetError().Error())
  
   fmt.Printf("\033[32m-------------\033[0m \033[34m%s\033[0m \033[32mrecovering ...\033[0m \n", w.GetWorkerID())
 
   go w.Work(wrks)
}

В этом примере у нас есть канал типа *workers.Worker. Канал передается в качестве аргумента методу Work(), этот канал будет использоваться для отправки сообщений из рабочей процедуры go. Затем цикл for используется для перебора канала wrks. Всякий раз, когда возникает паника, рабочий процесс, ответственный за панику, отправляет себя через канал. Цикл получает экземпляр рабочего процесса и выводит сообщение о том, что произошла паника, а также идентификатор рабочего процесса и причину паники (получается с помощью w.GetError().Error()). Внутри цикла рабочий метод Work() снова запускается в новой горутине, передавая канал works в качестве аргумента. Это гарантирует, что рабочий возобновит свою работу после возникновения паники.

Теперь посмотрим, какие изменения были внесены в метод Work().

Эта функция представляет собой обновленную версию метода Work(), которая включает механизм аварийного восстановления с использованием инструкции defer и анонимной функции. Сигнатура функции изменилась: теперь она принимает канал типа chan‹- *Worker в качестве аргумента в дополнение к получателю *Worker и по-прежнему возвращает ошибка. Оператор defer добавляется в начало функции. Он определяет анонимную функцию, которая выполняется, когда функция Work() возвращается из-за паники или нормального завершения. Анонимная функция внутри оператора defer восстанавливается после паники с помощью функции recover(). Если возникает паника, он проверяет, относится ли значение паники к типу error, и сохраняет его в поле w.Err; в противном случае создается новая ошибка со значением паники и сохраняется в поле w.Err. После выхода из паники и установки поля w.Err экземпляр Worker отправляется по каналу worker, сигнализируя основной Go-Routine что возникла паника. Остальная часть функции такая же, как и в первом примере, включая цикл for и генерацию паники при обнаружении простого числа.

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

Опять же, весь код, упомянутый в этой статье, и другие примеры можно найти в Github Repository.

Спасибо за чтение !. Обратная связь/вопросы всегда приветствуются.