Se ti serve una API per tassi di cambio in Go, la libreria standard basta quasi da sola. I pacchetti net/http, encoding/json e context sono sufficienti per costruire un client veloce e privo di dipendenze. La parte difficile non è la chiamata HTTP, ma tutto ciò che la circonda: timeout, gestione degli errori, caching, aritmetica monetaria e test. La maggior parte dei tutorial si ferma a http.Get e fmt.Println, il che va bene per una demo ma è rischioso in un flusso di pagamento.
In questa guida costruirai un client Golang piccolo ma pensato per la produzione per gli endpoint della documentazione dell'API Finexly, lo avvolgerai in una cache sicura per la concorrenza che sopravvive ai disservizi del fornitore, aggiungerai una funzione di conversione che lavora in unità minori intere e lo pubblicherai come servizio HTTP con test. Tutto compila con Go 1.22 o successivo e usa solo la libreria standard.
Perché Go è adatto ai dati sui tassi di cambio
I tassi di cambio vengono letti molto spesso, occupano poco spazio e cambiano lentamente alla scala di una richiesta web. Questo profilo si adatta benissimo a Go. Le goroutine permettono di servire migliaia di richieste concorrenti da un unico snapshot in memoria, un singolo binario statico è semplice da distribuire e context.Context offre annullamento e scadenze di prima classe per ogni chiamata in uscita.
L'obiettivo progettuale è semplice: i tuoi handler non dovrebbero mai chiamare direttamente l'API dei tassi. Leggono da una cache, e un solo percorso di aggiornamento sicuro dialoga con l'API. In questo modo la latenza resta stabile, la quota è protetta e un intoppo del fornitore non diventa un tuo disservizio. Per la teoria, la nostra guida a caching e gestione degli errori nelle API valutarie descrive la strategia; qui la implementiamo.
Passo 1: prova prima con cURL
Prima di scrivere Go, verifica che la tua chiave funzioni. Iscriviti gratis, copia la chiave ed esegui:
curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=$FINEXLY_API_KEY"{
"base": "USD",
"timestamp": 1762761600,
"rates": { "EUR": 0.92, "GBP": 0.80, "JPY": 151.2 }
}The numbers above are illustrative.
La risposta contiene tre campi: base (la valuta rispetto a cui sono espressi tutti i tassi), timestamp (secondi Unix) e rates (una mappa da codice ISO 4217 a tasso). Ogni valore in rates indica quante unità di quella valuta equivalgono a un'unità della base. Conserva la chiave in una variabile d'ambiente, mai nel controllo di versione:
export FINEXLY_API_KEY="your_key_here"Passo 2: un client tipizzato con timeout ed errori sicuri
Crea un modulo con go mod init fxdemo e inserisci il codice seguente in main.go. Definisce la struct della risposta, un APIError dedicato e un Client il cui metodo Latest accetta un context.Context:
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
)
type RatesResponse struct {
Base string `json:"base"`
Timestamp int64 `json:"timestamp"`
Rates map[string]float64 `json:"rates"`
}
type APIError struct {
Status int
Body string
}
func (e *APIError) Error() string {
return fmt.Sprintf("finexly: HTTP %d: %s", e.Status, e.Body)
}
type Client struct {
apiKey string
baseURL string
http *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
baseURL: "https://api.finexly.com/v1",
http: &http.Client{Timeout: 5 * time.Second},
}
}
func (c *Client) Latest(ctx context.Context, base string, symbols []string) (*RatesResponse, error) {
q := url.Values{}
q.Set("base", base)
if len(symbols) > 0 {
q.Set("symbols", strings.Join(symbols, ","))
}
q.Set("apikey", c.apiKey)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/latest?"+q.Encode(), nil)
if err != nil {
return nil, errors.New("finexly: build request failed")
}
resp, err := c.http.Do(req)
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
err = ue.Err // drop the URL, it contains the API key
}
return nil, fmt.Errorf("finexly: request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, &APIError{Status: resp.StatusCode, Body: strings.TrimSpace(string(body))}
}
var out RatesResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("finexly: decode response: %w", err)
}
return &out, nil
}Alcuni dettagli fanno la differenza tra una demo e codice di produzione:
- Un timeout rigido. Lo
http.Clientcon valori zero non ha timeout, quindi una connessione bloccata può trattenere una goroutine per sempre. Impostiamo5 * time.Seconde passiamo anche il contesto del chiamante, così una richiesta annullata dall'utente annulla la chiamata a monte. - Nessuna fuga della chiave nei log. Poiché la chiave viaggia nel parametro
apikey, lo*url.Errordi Go include nel messaggio l'URL completo, chiave compresa. Lo scartiamo conerrors.Ase conserviamo solo l'errore sottostante. Registrare quell'errore diventa sicuro. - Un
APIErrortipizzato. Il chiamante può usareerrors.Asper diramare in base aStatus, ad esempio rallentare con un429o dare l'allarme con un403. Il corpo viene letto tramiteio.LimitReader, così un server difettoso non può farti accumulare megabyte. - Un
baseURLconfigurabile. Non costa nulla e rende il client banale da testare, come vedrai al passo 6.
Passo 3: una cache sicura per la concorrenza con fallback su dati obsoleti
Chiamare l'API a ogni richiesta esaurirebbe la quota e aggiungerebbe latenza. Per la maggior parte delle applicazioni i tassi non cambiano in modo significativo nell'arco di pochi minuti, quindi vanno messi in cache. La cache seguente ha tre proprietà che gli esempi semplici con map e Mutex trascurano:
- I lettori non vengono mai bloccati da un aggiornamento. La chiamata di rete avviene fuori dal lock di lettura/scrittura.
- Una sola goroutine aggiorna alla volta. Un secondo mutex, più un nuovo controllo dopo averlo acquisito, evita la fiumana di richieste quando il TTL scade sotto carico.
- Meglio dati obsoleti che nessun dato. Se l'aggiornamento fallisce e abbiamo uno snapshot più vecchio, lo restituiamo con
stale=truee sarà il chiamante a decidere.
type Cache struct {
client *Client
base string
ttl time.Duration
mu sync.RWMutex
data *RatesResponse
fetchedAt time.Time
refreshMu sync.Mutex
}
func NewCache(c *Client, base string, ttl time.Duration) *Cache {
return &Cache{client: c, base: base, ttl: ttl}
}
func (c *Cache) fresh() (*RatesResponse, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
if c.data != nil && time.Since(c.fetchedAt) < c.ttl {
return c.data, true
}
return nil, false
}
// Get returns rates, and stale=true if it had to fall back to old data.
func (c *Cache) Get(ctx context.Context) (rates *RatesResponse, stale bool, err error) {
if d, ok := c.fresh(); ok {
return d, false, nil
}
c.refreshMu.Lock() // only one goroutine hits the API
defer c.refreshMu.Unlock()
if d, ok := c.fresh(); ok { // refreshed while we waited
return d, false, nil
}
d, err := c.client.Latest(ctx, c.base, nil)
if err != nil {
c.mu.RLock()
old := c.data
c.mu.RUnlock()
if old != nil {
return old, true, nil
}
return nil, false, err
}
c.mu.Lock()
c.data, c.fetchedAt = d, time.Now()
c.mu.Unlock()
return d, false, nil
}Nota che recuperiamo tutti i tassi della base in una sola richiesta (symbols è omesso). Una chiamata all'ora serve allora ogni coppia di valute di cui il tuo servizio ha bisogno, circa 720 chiamate al mese per un'istanza sempre attiva. Rientra nel piano gratuito da 1.000 richieste. Un TTL di dieci minuti comporterebbe circa 4.300 chiamate al mese, quindi intervalli più brevi richiedono un piano a pagamento; vedi i piani tariffari.
Passo 4: converti denaro senza sorprese in virgola mobile
Non salvare mai denaro in un float64. Salva unità minori intere (centesimi, pence, yen) e applica il tasso una sola volta, alla fine, con arrotondamento esplicito:
// Convert works in minor units (cents). It assumes both currencies use the
// same number of decimal places; see the ISO 4217 note in the article.
func Convert(amountMinor int64, from, to string, r *RatesResponse) (int64, error) {
rate := func(code string) (float64, error) {
if code == r.Base {
return 1, nil
}
v, ok := r.Rates[code]
if !ok || v <= 0 {
return 0, fmt.Errorf("no rate for %q", code)
}
return v, nil
}
f, err := rate(from)
if err != nil {
return 0, err
}
t, err := rate(to)
if err != nil {
return 0, err
}
return int64(math.Round(float64(amountMinor) * t / f)), nil
}Poiché i tassi sono quotati rispetto a una base, convertire EUR in GBP passa per quella base: si divide per il tasso di origine e si moltiplica per quello di destinazione. La funzione tratta inoltre la base stessa come un tasso esattamente pari a 1.
C'è una trappola concreta. La funzione sopra presume che le due valute abbiano lo stesso numero di decimali. Lo JPY ne ha 0, l'USD 2 e il KWD 3, quindi un sistema di produzione deve scalare in base all'esponente di ciascuna valuta. La nostra guida ai codici valuta ISO 4217 elenca l'esponente dell'unità minore per ogni codice. Per un controllo interattivo rapido dei numeri usa il convertitore di valute.
Passo 5: esponilo via HTTP
Ora colleghiamo tutto in un handler HTTP. L'handler analizza la richiesta, legge dalla cache usando il contesto della richiesta e segnala se i tassi erano obsoleti:
func convertHandler(cache *Cache) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
amount, err := strconv.ParseInt(q.Get("amount"), 10, 64)
if err != nil {
http.Error(w, "amount must be an integer in minor units", http.StatusBadRequest)
return
}
from, to := strings.ToUpper(q.Get("from")), strings.ToUpper(q.Get("to"))
rates, stale, err := cache.Get(r.Context())
if err != nil {
log.Printf("rates unavailable: %v", err)
http.Error(w, "rates unavailable", http.StatusBadGateway)
return
}
out, err := Convert(amount, from, to, rates)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"from": from, "to": to,
"amount_minor": amount, "converted_minor": out,
"rates_timestamp": rates.Timestamp, "stale": stale,
})
}
}
func main() {
apiKey := os.Getenv("FINEXLY_API_KEY")
if apiKey == "" {
log.Fatal("FINEXLY_API_KEY is not set")
}
cache := NewCache(NewClient(apiKey), "USD", time.Hour)
http.Handle("/convert", convertHandler(cache))
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Avvialo e prova a convertire 100,00 USD in EUR (10.000 centesimi):
go run . &
curl "http://localhost:8080/convert?amount=10000&from=USD&to=EUR"La risposta include rates_timestamp e un flag stale, così i sistemi a valle possono mostrare "tassi aggiornati al..." oppure rifiutarsi di quotare quando i dati sono troppo vecchi. Questo solo campo è spesso la prima cosa che chiedono revisori e team finanziari.
Passo 6: testa senza toccare la rete
Dato che l'URL di base è iniettabile, puoi testare l'intera logica di client e cache con httptest.NewServer. Questo test dimostra il comportamento più importante: quando l'API inizia a fallire, la cache serve dati obsoleti invece di un errore.
func TestLatestAndStaleFallback(t *testing.T) {
healthy := true
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !healthy {
http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError)
return
}
w.Write([]byte(`{"base":"USD","timestamp":1,"rates":{"EUR":0.92}}`))
}))
defer srv.Close()
c := NewClient("test-key")
c.baseURL = srv.URL
cache := NewCache(c, "USD", time.Millisecond)
if _, stale, err := cache.Get(context.Background()); err != nil || stale {
t.Fatalf("first call: err=%v stale=%v", err, stale)
}
healthy = false
time.Sleep(5 * time.Millisecond)
r, stale, err := cache.Get(context.Background())
if err != nil || !stale || r.Rates["EUR"] != 0.92 {
t.Fatalf("expected stale fallback, got err=%v stale=%v", err, stale)
}
}
func TestConvert(t *testing.T) {
r := &RatesResponse{Base: "USD", Rates: map[string]float64{"EUR": 0.92, "GBP": 0.80}}
got, err := Convert(10000, "EUR", "GBP", r)
if err != nil || got != 8696 {
t.Fatalf("got %d err %v", got, err)
}
}Eseguilo con go test ./.... Test come questo girano in millisecondi, non richiedono chiavi API e possono far parte della CI. Con lo stesso stile puoi aggiungere casi per risposte 429, JSON malformato e server lenti (per verificare il timeout).
Checklist per la produzione
Prima del rilascio, ripassa questi punti:
- Scegli un TTL sensato. Un'ora va bene per la fatturazione e per i prezzi mostrati nell'e-commerce, e tiene un'istanza nel piano gratuito. Funzioni di quotazione o trading possono richiedere minuti o secondi. Adatta il TTL al rischio di business, poi scegli il piano.
- Mostra il timestamp del tasso ovunque un cliente veda un importo convertito e salva il tasso usato in ogni transazione, così da poterla riprodurre in seguito.
- Imposta un allarme sull'obsolescenza. Emetti una metrica quando
stale=truepersiste oltre una soglia. Una cache che serve in silenzio dati vecchi di un giorno è peggio di un errore. - Scalda la cache all'avvio e fai fallire i controlli di readiness se il primo recupero fallisce, così una chiave errata viene scoperta al deploy.
- Gestisci il
429con backoff invece di riprovare subito. Se lo vedi spesso, il TTL è troppo breve oppure più istanze si aggiornano ciascuna per conto proprio. Una cache condivisa come Redis risolve il secondo caso. - Confronta ogni tanto i fornitori. Se stai valutando fonti di dati, la nostra pagina per confrontare le API valutarie e la panoramica sulla API valutaria gratuita mostrano cosa osservare.
Se lavori con più linguaggi, gli stessi schemi compaiono nel nostro tutorial Rust e nel tutorial Python.
Domande frequenti
Esiste un'API gratuita per i tassi di cambio per Go?
Sì. Finexly offre un piano gratuito con 1.000 richieste al mese e il client qui sopra non richiede pacchetti di terze parti. Con una cache di un'ora, un'istanza sempre attiva usa circa 720 richieste al mese, lasciando margine. Ricorda che ogni istanza aggiuntiva si aggiorna per conto proprio. Consulta i piani tariffari per i limiti attuali.
Devo usare float64 per i tassi di cambio in Go?
Usare float64 per il tasso è in genere accettabile, dato che i tassi sono approssimazioni con poche cifre significative. Usarlo per gli importi no. Conserva gli importi come unità minori intere, oppure usa una libreria decimale, e arrotonda una sola volta alla fine.
Con quale frequenza devo aggiornare i tassi di cambio?
Dipende dalla tua tolleranza al rischio. Per prezzi mostrati e report bastano di solito da 15 a 60 minuti. La fatturazione spesso blocca un tasso per documento. Tutto ciò che muove denaro in tempo reale dovrebbe salvare il tasso esatto e il timestamp usati, qualunque sia l'intervallo di aggiornamento.
Come evito che la mia chiave API finisca nei log di Go?
Quando la chiave è inviata come parametro di query, gli errori restituiti da http.Client incorporano l'URL completo. Scarta lo *url.Error con errors.As e registra solo il campo Err, come fa il client di questa guida. Evita inoltre di registrare gli URL completi delle richieste nei middleware.
Posso recuperare più valute di base in parallelo?
Sì. Usa golang.org/x/sync/errgroup con SetLimit per limitare il parallelismo e condividi lo stesso Client, che è sicuro per l'uso concorrente. Nella maggior parte dei casi, una sola valuta di base più il calcolo dei cambi incrociati del passo 4 è più semplice ed economico.
Prossimi passi
Pronto a integrare i tassi di cambio in tempo reale nel tuo servizio Go? Ottieni la tua chiave API Finexly gratuita: nessuna carta di credito richiesta. Inizia con 1.000 richieste gratuite al mese e passa a un piano superiore man mano che cresci.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →