Scrivere Client dei Servizi
Quando si costruiscono microservizi, una sfida comune è come strutturare la comunicazione tra i servizi. Questa sezione copre le best practice per scrivere client per i servizi Goa, concentrandosi sulla creazione di implementazioni manutenibili e testabili.
Filosofia di Design dei Client
L’approccio raccomandato per costruire client per i servizi Goa segue questi principi chiave:
- Responsabilità Singola: Creare un client per ogni servizio downstream, invece di una libreria client condivisa
- Interfacce Ristrette: Definire interfacce che espongono solo i metodi necessari al servizio consumatore
- Indipendenza dall’Implementazione: Supportare diversi protocolli di trasporto (gRPC, HTTP) dietro la stessa interfaccia
- Testabilità: Abilitare un facile mocking per i test attraverso interfacce ben definite
Questo approccio aiuta a evitare la creazione di monoliti distribuiti dove i servizi diventano strettamente accoppiati attraverso librerie client condivise.
Struttura del Client
Un tipico client di servizio Goa consiste in:
- Un’interfaccia client che definisce il contratto del servizio
- Tipi di dati che rappresentano i modelli di dominio
- Un’implementazione concreta che usa il client Goa generato
- Funzioni factory per creare istanze del client
Vediamo un esempio completo di un client per un servizio di previsioni meteo:
package forecaster
import (
    "context"
    "google.golang.org/grpc"
    "goa.design/clue/debug"
    genforecast "goa.design/clue/example/weather/services/forecaster/gen/forecaster"
    gengrpcclient "goa.design/clue/example/weather/services/forecaster/gen/grpc/forecaster/client"
)
type (
    // Client è un client per il servizio di previsioni.
    Client interface {
        // GetForecast ottiene la previsione per la posizione data.
        GetForecast(ctx context.Context, lat, long float64) (*Forecast, error)
    }
    // Forecast rappresenta la previsione per una data posizione.
    Forecast struct {
        // Location è la posizione della previsione.
        Location *Location
        // Periods sono le previsioni per la posizione.
        Periods []*Period
    }
    // Location rappresenta la posizione geografica di una previsione.
    Location struct {
        // Lat è la latitudine della posizione.
        Lat float64
        // Long è la longitudine della posizione.
        Long float64
        // City è la città della posizione.
        City string
        // State è lo stato della posizione.
        State string
    }
    // Period rappresenta un periodo di previsione.
    Period struct {
        // Name è il nome del periodo di previsione.
        Name string
        // StartTime è l'ora di inizio del periodo di previsione in formato RFC3339.
        StartTime string
        // EndTime è l'ora di fine del periodo di previsione in formato RFC3339.
        EndTime string
        // Temperature è la temperatura del periodo di previsione.
        Temperature int
        // TemperatureUnit è l'unità di temperatura del periodo di previsione.
        TemperatureUnit string
        // Summary è il riepilogo del periodo di previsione.
        Summary string
    }
    // client è l'implementazione del client.
    client struct {
        genc *genforecast.Client
    }
)
// New istanzia un nuovo client del servizio previsioni.
func New(cc *grpc.ClientConn) Client {
    c := gengrpcclient.NewClient(cc, grpc.WaitForReady(true))
    forecast := debug.LogPayloads(debug.WithClient())(c.Forecast())
    return &client{genc: genforecast.NewClient(forecast)}
}
// GetForecast restituisce la previsione per la posizione data.
func (c *client) GetForecast(ctx context.Context, lat, long float64) (*Forecast, error) {
    res, err := c.genc.Forecast(ctx, &genforecast.ForecastPayload{Lat: lat, Long: long})
    if err != nil {
        return nil, err
    }
    l := Location(*res.Location)
    ps := make([]*Period, len(res.Periods))
    for i, p := range res.Periods {
        pval := Period(*p)
        ps[i] = &pval
    }
    return &Forecast{&l, ps}, nil
}
Analizziamo i componenti chiave:
Interfaccia Client
L’interfaccia definisce il contratto che i consumatori useranno:
type Client interface {
    GetForecast(ctx context.Context, lat, long float64) (*Forecast, error)
}
Questa interfaccia ristretta espone solo i metodi necessari ai consumatori, nascondendo i dettagli implementativi e rendendo più facile la manutenzione e il testing.
Tipi di Dominio
Il pacchetto client definisce i propri tipi di dominio (Forecast, Location,
Period) invece di esporre i tipi generati. Questo fornisce:
- Isolamento dai cambiamenti del codice generato
- Un’API più pulita e focalizzata
- Migliore controllo sul modello dati esposto
Implementazione
L’implementazione concreta usa internamente il client Goa generato mentre presenta l’interfaccia semplificata ai consumatori:
type client struct {
    genc *genforecast.Client
}
Funzione Factory
La funzione New istanzia il client con la configurazione appropriata
specifica del trasporto:
func New(cc *grpc.ClientConn) Client {
    c := gengrpcclient.NewClient(cc, grpc.WaitForReady(true))
    forecast := debug.LogPayloads(debug.WithClient())(c.Forecast())
    return &client{genc: genforecast.NewClient(forecast)}
}
Client HTTP
Mentre l’esempio sopra mostra un client gRPC, i client HTTP seguono lo stesso pattern ma con una diversa inizializzazione. Vediamo in dettaglio come funzionano i client HTTP.
Client HTTP Generato da Goa
Goa genera un’implementazione completa del client HTTP per il tuo servizio. Ecco come appare un tipico client HTTP generato:
// Client elenca i client HTTP degli endpoint del servizio.
type Client struct {
    // ForecastDoer è il client HTTP usato per fare richieste all'endpoint forecast.
    ForecastDoer goahttp.Doer
    // Campi di configurazione
    RestoreResponseBody bool
    scheme             string
    host               string
    encoder            func(*http.Request) goahttp.Encoder
    decoder            func(*http.Response) goahttp.Decoder
}
// NewClient istanzia client HTTP per tutti i server del servizio.
func NewClient(
    scheme string,
    host string,
    doer goahttp.Doer,
    enc func(*http.Request) goahttp.Encoder,
    dec func(*http.Response) goahttp.Decoder,
    restoreBody bool,
) *Client {
    return &Client{
        ForecastDoer:        doer,
        RestoreResponseBody: restoreBody,
        scheme:             scheme,
        host:               host,
        decoder:            dec,
        encoder:            enc,
    }
}
// Forecast restituisce un endpoint che fa richieste HTTP al server forecast del servizio.
func (c *Client) Forecast() goa.Endpoint {
    var (
        decodeResponse = DecodeForecastResponse(c.decoder, c.RestoreResponseBody)
    )
    return func(ctx context.Context, v any) (any, error) {
        req, err := c.BuildForecastRequest(ctx, v)
        if err != nil {
            return nil, err
        }
        resp, err := c.ForecastDoer.Do(req)
        if err != nil {
            return nil, goahttp.ErrRequestError("front", "forecast", err)
        }
        return decodeResponse(resp)
    }
}
Il client generato fornisce:
- Un’interfaccia Doerper ogni endpoint che permette la personalizzazione del comportamento del client HTTP
- Codifica delle richieste e decodifica delle risposte integrate
- Builder di richieste e decoder di risposte specifici per endpoint
- Supporto per middleware attraverso l’interfaccia Doer
Creare la Tua Interfaccia Client
Per creare un’interfaccia client pulita usando il client HTTP generato, scriveresti:
func NewHTTP(doer goa.Doer) Client {
    // Crea il client HTTP generato
    c := genhttpclient.NewClient(
        "http",                    // schema
        "weather-service:8080",    // host
        doer,                      // client HTTP
        goahttp.RequestEncoder,    // encoder richieste
        goahttp.ResponseDecoder,   // decoder risposte
        false,                     // ripristina body risposta
    )
    // Crea endpoint usando il client generato
    forecast := debug.LogPayloads(debug.WithClient())(c.Forecast())
    
    // Ritorna il client con l'endpoint configurato
    return &client{
        genc: genforecast.NewClient(forecast),
    }
}
Best Practice
- Isolamento: - Mantieni i client in pacchetti separati
- Definisci interfacce specifiche per il consumatore
- Evita dipendenze condivise tra client
- Usa modelli di dominio specifici del client
 
- Configurazione: - Rendi la configurazione del client flessibile
- Supporta diversi protocolli di trasporto
- Permetti la personalizzazione del comportamento
- Usa valori di default sensati
 
- Gestione Errori: - Definisci tipi di errore chiari
- Fornisci contesto negli errori
- Gestisci errori di rete e timeout
- Implementa retry quando appropriato
 
- Testing: - Scrivi test unitari per il client
- Usa mock per le dipendenze
- Testa scenari di errore
- Verifica il comportamento di timeout
 
Per Saperne di Più
Per maggiori informazioni sulla creazione di client:
- Pacchetto Client di Goa Documentazione completa del pacchetto client di Goa 
- Pattern di Comunicazione Pattern comuni per la comunicazione tra servizi 
- Gestione Errori Client Best practice per la gestione degli errori nei client Go