Questa guida spiega come gli errori si propagano attraverso i diversi layer di un servizio Goa, dalla logica di business fino al client.
La propagazione degli errori in Goa segue un percorso chiaro:
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
}
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:
ErrorResult
: Usa il nome dell’errore dalla funzione MakeXXX
generatastruct:error:name
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:
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)
}
}
Wrapping degli Errori
fmt.Errorf("...%w", err)
Propagazione Coerente
Considerazioni sul Trasporto
Esperienza del Client
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)
}
}
}
Il sistema di propagazione degli errori di Goa assicura che: