История из боевых распределённых систем. Один кривой подписчик уронил доставку всем остальным, потому что паника в горутине без
recover завершает весь процесс. Острая грань языка, про которую легко забыть.➡️ Что случилось
Был шлюз вебхуков, который параллельно рассылал события десяткам подписчиков. У одного оказался кривой URL, который в редком случае приводил к панике в HTTP-клиенте вместо ошибки.
Раздача шла в голых горутинах без восстановления:
func fanOut(events []Event, subscribers []Subscriber) {
var wg sync.WaitGroup
for _, sub := range subscribers {
wg.Add(1)
go func(s Subscriber) {
defer wg.Done()
deliver(s, events) // паника здесь убивает весь процесс
}(sub)
}
wg.Wait()
}В Go паника в любой горутине, которую не перехватили через
recover, завершает всю программу. Не важно, сколько других горутин в этот момент делают полезную работу. Про это легко забыть, потому что в туториалах паника обычно происходит в главной горутине, где последствия очевидны. Здесь же баг одного подписчика оборвал доставку всем.
➡️ Как починили
Изоляция паники стала структурным паттерном, а не заплаткой по случаю. Появилась обёртка
safeGo:func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered panic: %v\n%s", r, debug.Stack())
}
}()
fn()
}()
}
func fanOut(events []Event, subscribers []Subscriber) {
var wg sync.WaitGroup
for _, sub := range subscribers {
wg.Add(1)
safeGo(func() {
defer wg.Done()
deliver(sub, events)
})
}
wg.Wait()
}Теперь каждая долгоживущая или fan-out горутина проходит через обёртку вроде
safeGo, и серверные gRPC-интерсепторы получили то же самое. Восстановление после паники переехало с верхнего HTTP-хендлера на каждую горутину, которую вы заводите сами.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProduction