Можно ли в родительской горутине отловить панику, возникшую в дочерней?

Ответ

Нет, напрямую отловить панику из дочерней горутины в родительской с помощью recover нельзя.

Почему?

Механизм panic/recover работает в пределах одной горутины. Паника распространяется вверх по стеку вызовов текущей горутины, и recover может её остановить только в том случае, если он был вызван в defer функции в той же горутине.

Если в горутине происходит паника, и она не обрабатывается с помощью recover, программа аварийно завершается.

Правильные подходы к обработке паники в дочерних горутинах

Ключевая идея — дочерняя горутина должна сама обработать свою панику и сообщить о ней родительской через один из механизмов синхронизации.

1. recover + канал для передачи ошибки

Это основной и самый надежный паттерн. Дочерняя горутина использует defer и recover, а в случае паники отправляет информацию о ней в виде ошибки через канал.

package main

import (
    "fmt"
    "time"
)

func main() {
    errChan := make(chan error, 1) // Буферизированный канал

    go func() {
        // defer-функция для перехвата паники
        defer func() {
            if r := recover(); r != nil {
                // Паника произошла, отправляем ее как ошибку в канал
                errChan <- fmt.Errorf("дочерняя горутина запаниковала: %v", r)
            }
        }()

        fmt.Println("Дочерняя горутина начинает работу...")
        // Имитация паники
        panic("что-то пошло не так")
    }()

    // Родительская горутина ждет сообщения из канала
    select {
    case err := <-errChan:
        fmt.Printf("Получена ошибка от дочерней горутины: %vn", err)
    case <-time.After(2 * time.Second):
        fmt.Println("Дочерняя горутина завершилась без ошибок.")
    }
}

2. Использование errgroup

Пакет golang.org/x/sync/errgroup элегантно решает эту проблему. Он автоматически обрабатывает панику в любой из запущенных им горутин, превращает её в ошибку и возвращает из метода Wait().

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
)

func main() {
    g, _ := errgroup.WithContext(context.Background())

    g.Go(func() error {
        fmt.Println("Запускаем горутину, которая вызовет панику...")
        panic("ой, все сломалось")
        // Эта горутина никогда не вернет nil
        return nil
    })

    // g.Wait() заблокируется до завершения горутины
    // и вернет ошибку, если была паника или горутина вернула ошибку.
    if err := g.Wait(); err != nil {
        fmt.Printf("errgroup перехватил ошибку: %vn", err)
    }
}

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