Propagazione degli Errori
Questa guida spiega come gli errori si propagano attraverso i diversi layer di un servizio Goa, dalla logica di business fino al client.
Panoramica
La propagazione degli errori in Goa segue un percorso chiaro:
- La logica di business genera un errore
- L’errore viene abbinato alla sua definizione nel design
- Il layer di trasporto trasforma l’errore
- Il client riceve e interpreta l’errore
Flusso degli Errori
1. Layer della Logica di Business
Gli errori tipicamente originano nella tua implementazione del servizio:
func (s *paymentService) Process(ctx context.Context, p *payment.ProcessPayload) (*payment.ProcessResult, error) {
// La logica di business può restituire errori in diversi modi:
// 1. Usando funzioni helper generate (per ErrorResult)
if !hasEnoughFunds(p.Amount) {
return nil, payment.MakeInsufficientFunds(
fmt.Errorf("saldo conto %d sotto l'importo richiesto %d", balance, p.Amount))
}
// 2. Restituendo tipi di errore personalizzati
if err := validateCard(p.Card); err != nil {
return nil, &payment.PaymentError{
Name: "card_expired",
Message: err.Error(),
}
}
// 3. Propagando errori da servizi downstream
result, err := s.processor.ProcessPayment(ctx, p)
if err != nil {
// Avvolgi gli errori esterni nei tuoi errori di dominio
return nil, payment.MakeProcessingFailed(fmt.Errorf("errore processore pagamenti: %w", err))
}
return result, nil
}
2. Abbinamento degli Errori
Il runtime di Goa abbina gli errori restituiti alle loro definizioni nel design:
var _ = Service("payment", func() {
// Gli errori definiti qui sono abbinati per nome
Error("insufficient_funds")
Error("card_expired")
Error("processing_failed", func() {
// Le proprietà influenzano la gestione degli errori
Temporary()
Fault()
})
})
Il processo di abbinamento:
- Per
ErrorResult: Usa il nome dell’errore dalla funzioneMakeXXXgenerata - Per tipi personalizzati: Usa il campo marcato con
struct:error:name - Per errori sconosciuti: Trattati come errori interni del server
3. Layer di Trasporto
Una volta abbinati, gli errori vengono trasformati secondo regole specifiche del trasporto:
var _ = Service("payment", func() {
HTTP(func() {
// Regole di mappatura HTTP
Response("insufficient_funds", StatusPaymentRequired)
Response("card_expired", StatusUnprocessableEntity)
Response("processing_failed", StatusServiceUnavailable)
})
GRPC(func() {
// Regole di mappatura gRPC
Response("insufficient_funds", CodeFailedPrecondition)
Response("card_expired", CodeInvalidArgument)
Response("processing_failed", CodeUnavailable)
})
})
Il layer di trasporto:
- Applica il codice di stato appropriato
- Formatta il messaggio di errore e i dettagli
- Serializza la risposta
4. Ricezione del Client
I client ricevono errori fortemente tipizzati che corrispondono al design:
client := payment.NewClient(endpoint)
result, err := client.Process(ctx, payload)
if err != nil {
switch e := err.(type) {
case *payment.InsufficientFundsError:
// Gestisci fondi insufficienti (include proprietà dell'errore)
if e.Temporary {
return retry(ctx, payload)
}
return promptForTopUp(e.Message)
case *payment.CardExpiredError:
// Gestisci carta scaduta
return promptForNewCard(e.Message)
case *payment.ProcessingFailedError:
// Gestisci fallimento elaborazione
if e.Temporary {
return retryWithBackoff(ctx, payload)
}
return reportSystemError(e)
default:
// Gestisci errori inaspettati
return handleUnknownError(err)
}
}
Best Practice
Wrapping degli Errori
- Avvolgi gli errori esterni nei tuoi errori di dominio
- Preserva la causa radice usando
fmt.Errorf("...%w", err) - Aggiungi contesto rilevante per il tuo dominio
Propagazione Coerente
- Usa funzioni helper generate quando possibile
- Mantieni le proprietà degli errori attraverso la catena
- Non mischiare tipi di errore inutilmente
Considerazioni sul Trasporto
- Definisci codici di stato appropriati per ogni trasporto
- Includi headers/metadata rilevanti
- Considera i requisiti del client
Esperienza del Client
- Fornisci errori fortemente tipizzati
- Includi contesto sufficiente per la gestione
- Documenta strategie di retry
Esempio di Trasformazione degli Errori
Ecco un esempio completo di come un errore si trasforma attraverso il sistema:
// 1. Logica di Business (Implementazione del Servizio)
if !hasEnoughFunds(amount) {
return nil, payment.MakeInsufficientFunds(
fmt.Errorf("saldo %d sotto il richiesto %d", balance, amount))
}
// 2. Definizione dell'Errore (Design)
var _ = Service("payment", func() {
Error("insufficient_funds", func() {
Description("Il conto non ha fondi sufficienti")
Temporary() // Può riprovare dopo ricarica
})
})
// 3. Mappatura del Trasporto (Design)
HTTP(func() {
Response("insufficient_funds", StatusPaymentRequired)
})
// 4. Ricezione del Client
result, err := client.Process(ctx, payload)
if err != nil {
if e, ok := err.(*payment.InsufficientFundsError); ok {
if e.Temporary {
// Attendi la durata dell'header retry-after
time.Sleep(retryAfter)
return retry(ctx, payload)
}
}
}
Conclusione
Il sistema di propagazione degli errori di Goa assicura che:
- Gli errori mantengano il loro significato semantico attraverso i layer
- I dettagli specifici del trasporto siano gestiti automaticamente
- I client ricevano errori fortemente tipizzati e azionabili
- La gestione degli errori rimanga coerente attraverso la tua API