Wenn Sie eine Wechselkurs-API in Go benötigen, reicht die Standardbibliothek fast vollständig aus. Die Pakete net/http, encoding/json und context genügen, um einen schnellen, abhängigkeitsfreien Client zu bauen. Schwierig ist nicht der HTTP-Aufruf, sondern alles drumherum: Timeouts, Fehlerbehandlung, Caching, Geldarithmetik und Tests. Die meisten Tutorials enden bei http.Get und fmt.Println. Für eine Demo ist das in Ordnung, in einem Zahlungsablauf ist es riskant.
In dieser Anleitung bauen Sie einen kleinen, aber produktionsorientierten Golang-Client für die Endpunkte der Finexly-API-Dokumentation, verpacken ihn in einen nebenläufigkeitssicheren Cache, der Ausfälle des Anbieters übersteht, ergänzen eine Umrechnungsfunktion auf Basis ganzzahliger Untereinheiten und stellen alles als HTTP-Dienst mit Tests bereit. Der gesamte Code kompiliert mit Go 1.22 oder neuer und verwendet ausschließlich die Standardbibliothek.
Warum Go gut zu Wechselkursdaten passt
Wechselkurse werden sehr häufig gelesen, sind klein und ändern sich auf der Zeitskala einer Webanfrage nur langsam. Dieses Profil passt hervorragend zu Go. Goroutinen machen es günstig, Tausende paralleler Anfragen aus einem einzigen In-Memory-Snapshot zu bedienen, ein einzelnes statisches Binary lässt sich einfach ausrollen, und context.Context bietet erstklassige Abbrüche und Deadlines für jeden ausgehenden Aufruf.
Das Designziel ist einfach: Ihre Request-Handler sollten die Kurs-API nie direkt aufrufen. Sie lesen aus einem Cache, und ein einziger sicherer Aktualisierungspfad spricht mit der API. So bleibt die Latenz konstant, Ihr Kontingent geschützt, und ein Aussetzer des Anbieters wird nicht zu Ihrem Ausfall. Die Theorie dazu finden Sie in unserem Leitfaden zu Caching und Fehlerbehandlung bei Währungs-APIs; hier setzen wir sie um.
Schritt 1: Zuerst mit cURL testen
Bevor Sie Go schreiben, prüfen Sie, ob Ihr Schlüssel funktioniert. Registrieren Sie sich kostenlos, kopieren Sie Ihren Schlüssel und führen Sie aus:
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.
Die Antwort enthält drei Felder: base (die Währung, auf die sich alle Kurse beziehen), timestamp (Unix-Sekunden) und rates (eine Zuordnung von ISO-4217-Code zu Kurs). Jeder Wert in rates gibt an, wie viele Einheiten dieser Währung einer Einheit der Basiswährung entsprechen. Legen Sie den Schlüssel in einer Umgebungsvariable ab, niemals in der Versionsverwaltung:
export FINEXLY_API_KEY="your_key_here"Schritt 2: Ein typisierter Client mit Timeouts und sicheren Fehlern
Erstellen Sie ein Modul mit go mod init fxdemo und legen Sie den folgenden Code in main.go ab. Er definiert die Antwortstruktur, einen eigenen APIError und einen Client, dessen Methode Latest einen context.Context entgegennimmt:
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
}Einige Details unterscheiden eine Demo von Produktionscode:
- Ein hartes Timeout. Der
http.Clientmit Nullwerten hat kein Timeout, sodass eine hängende Verbindung eine Goroutine für immer festhalten kann. Wir setzen5 * time.Secondund reichen zusätzlich den Kontext des Aufrufers durch, damit eine vom Nutzer abgebrochene Anfrage auch den Upstream-Aufruf abbricht. - Kein Schlüssel-Leak in Logs. Da der Schlüssel als Query-Parameter
apikeyübertragen wird, enthält Gos*url.Errordie vollständige URL samt Schlüssel in der Fehlermeldung. Wir packen ihn miterrors.Asaus und behalten nur den zugrunde liegenden Fehler. Diesen Fehler zu loggen ist damit unbedenklich. - Ein typisierter
APIError. Aufrufer können miterrors.Asanhand vonStatusverzweigen, etwa bei429zurückfahren oder bei403alarmieren. Der Body wird überio.LimitReadergelesen, damit ein fehlerhafter Upstream Sie nicht Megabytes puffern lässt. - Eine konfigurierbare
baseURL. Das kostet nichts und macht den Client mühelos testbar, wie Sie in Schritt 6 sehen werden.
Schritt 3: Ein nebenläufigkeitssicherer Cache mit Stale-Fallback
Bei jeder Anfrage die API aufzurufen würde Ihr Kontingent verbrauchen und jede Antwort verzögern. Für die meisten Anwendungen ändern sich Kurse innerhalb weniger Minuten nicht nennenswert, also werden sie gecacht. Der folgende Cache hat drei Eigenschaften, die einfache Beispiele mit map und Mutex übersehen:
- Leser werden nie durch eine Aktualisierung blockiert. Der Netzwerkaufruf findet außerhalb der Lese-/Schreibsperre statt.
- Nur eine Goroutine aktualisiert gleichzeitig. Ein zweiter Mutex samt erneuter Prüfung nach dem Erwerb verhindert einen Ansturm, wenn die TTL unter Last abläuft.
- Veraltete Daten sind besser als gar keine. Schlägt die Aktualisierung fehl und liegt ein älterer Snapshot vor, geben wir ihn mit
stale=truezurück, damit der Aufrufer entscheiden kann.
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
}Beachten Sie, dass wir alle Kurse der Basiswährung in einer Anfrage abrufen (symbols entfällt). Ein Aufruf pro Stunde bedient dann jedes Währungspaar, das Ihr Dienst braucht, also etwa 720 Aufrufe pro Monat für eine dauerhaft laufende Instanz. Das passt in den kostenlosen Tarif mit 1.000 Anfragen. Eine TTL von zehn Minuten ergäbe etwa 4.300 Aufrufe pro Monat, kürzere Intervalle erfordern also einen kostenpflichtigen Tarif; siehe die Preispläne.
Schritt 4: Geld ohne Gleitkomma-Überraschungen umrechnen
Speichern Sie Geld niemals in einem float64. Speichern Sie ganzzahlige Untereinheiten (Cent, Pence, Yen) und wenden Sie den Kurs nur einmal am Ende mit ausdrücklicher Rundung an:
// 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
}Da Kurse gegenüber einer Basis notiert werden, läuft die Umrechnung von EUR in GBP über diese Basis: durch den Quellkurs teilen, mit dem Zielkurs multiplizieren. Die Funktion behandelt die Basis selbst als Kurs von genau 1.
Es gibt eine echte Falle. Die obige Funktion nimmt an, dass beide Währungen gleich viele Nachkommastellen haben. JPY hat 0, USD 2 und KWD 3, ein Produktivsystem muss also nach dem Exponenten jeder Währung skalieren. Unser Leitfaden zu ISO-4217-Währungscodes nennt den Exponenten der Untereinheit für jeden Code. Für eine schnelle interaktive Kontrolle nutzen Sie den Währungsrechner.
Schritt 5: Als HTTP-Dienst bereitstellen
Nun verbinden wir alles in einem HTTP-Handler. Der Handler parst die Anfrage, liest mit dem Request-Kontext aus dem Cache und meldet, ob die Kurse veraltet waren:
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))
}Starten Sie ihn und probieren Sie die Umrechnung von 100,00 USD in EUR (10.000 Cent):
go run . &
curl "http://localhost:8080/convert?amount=10000&from=USD&to=EUR"Die Antwort enthält rates_timestamp und ein stale-Flag, sodass nachgelagerte Systeme „Kurse vom ..." anzeigen oder keine Preise mehr nennen können, wenn die Daten zu alt sind. Dieses eine Feld verlangen Prüfer und Finanzteams oft als Erstes.
Schritt 6: Testen, ohne das Netzwerk zu berühren
Da die Basis-URL injizierbar ist, können Sie die gesamte Client- und Cache-Logik gegen httptest.NewServer testen. Dieser Test belegt das wichtigste Verhalten: Fällt die API aus, liefert der Cache veraltete Daten statt eines Fehlers.
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)
}
}Führen Sie ihn mit go test ./... aus. Solche Tests laufen in Millisekunden, benötigen keinen API-Key und passen in jede CI-Pipeline. Im selben Stil können Sie Fälle für 429-Antworten, fehlerhaftes JSON und langsame Server (zur Prüfung des Timeouts) ergänzen.
Checkliste für den Produktivbetrieb
Gehen Sie vor dem Release diese Punkte durch:
- Wählen Sie eine sinnvolle TTL. Eine Stunde passt für Rechnungsstellung und angezeigte Shop-Preise und hält eine Instanz im kostenlosen Tarif. Angebots- oder Trading-Funktionen brauchen eventuell Minuten oder Sekunden. Richten Sie die TTL am Geschäftsrisiko aus und wählen Sie dann den Tarif.
- Zeigen Sie den Kurs-Zeitstempel an, wo immer Kunden einen umgerechneten Betrag sehen, und speichern Sie bei jeder Transaktion den verwendeten Kurs, damit sie später nachvollziehbar ist.
- Alarmieren Sie bei veralteten Daten. Senden Sie eine Metrik, wenn
stale=trueüber einen Schwellenwert hinaus anhält. Ein Cache, der stillschweigend einen Tag alte Daten ausliefert, ist schlimmer als ein Fehler. - Wärmen Sie den Cache beim Start auf und lassen Sie Readiness-Checks fehlschlagen, wenn der erste Abruf scheitert, damit ein falscher Schlüssel schon beim Deployment auffällt.
- Behandeln Sie
429mit Backoff, statt sofort erneut zu versuchen. Tritt es häufig auf, ist Ihre TTL zu kurz oder mehrere Instanzen aktualisieren unabhängig voneinander. Ein gemeinsamer Cache wie Redis löst den zweiten Fall. - Vergleichen Sie Anbieter gelegentlich. Bei der Bewertung von Datenquellen zeigen unsere Seite Währungs-APIs vergleichen und der Überblick zur kostenlosen Währungs-API, worauf es ankommt.
Wenn Sie mehrere Sprachen nutzen, finden Sie dieselben Muster in unserem Rust-Tutorial und Python-Tutorial.
Häufig gestellte Fragen
Gibt es eine kostenlose Wechselkurs-API für Go?
Ja. Finexly bietet einen kostenlosen Tarif mit 1.000 Anfragen pro Monat, und der Client oben kommt ohne Drittanbieter-Pakete aus. Mit einem Stunden-Cache verbraucht eine dauerhaft laufende Instanz etwa 720 Anfragen pro Monat, das lässt Spielraum. Bedenken Sie, dass jede weitere Instanz eigenständig aktualisiert. Aktuelle Limits finden Sie in den Preisplänen.
Sollte ich float64 für Wechselkurse in Go verwenden?
float64 für den Kurs ist in der Regel akzeptabel, da Kurse Näherungswerte mit wenigen signifikanten Stellen sind. Für Geldbeträge ist es das nicht. Speichern Sie Beträge als ganzzahlige Untereinheiten oder nutzen Sie eine Dezimalbibliothek und runden Sie nur einmal am Ende.
Wie oft sollte ich Wechselkurse aktualisieren?
Das hängt von Ihrer Risikotoleranz ab. Für angezeigte Preise und Berichte genügen meist 15 bis 60 Minuten. Bei der Rechnungsstellung wird oft ein Kurs pro Dokument festgeschrieben. Alles, was in Echtzeit Geld bewegt, sollte den exakt verwendeten Kurs und Zeitstempel speichern, unabhängig vom Aktualisierungsintervall.
Wie verhindere ich, dass mein API-Key in Go-Logs auftaucht?
Wird ein Schlüssel als Query-Parameter gesendet, enthalten die Fehler von http.Client die vollständige URL. Packen Sie *url.Error mit errors.As aus und loggen Sie nur das Feld Err, wie es der Client in dieser Anleitung tut. Vermeiden Sie außerdem, vollständige Request-URLs in Middleware zu protokollieren.
Kann ich mehrere Basiswährungen parallel abrufen?
Ja. Nutzen Sie golang.org/x/sync/errgroup mit SetLimit, um die Parallelität zu begrenzen, und teilen Sie sich denselben Client, der für nebenläufige Nutzung sicher ist. In den meisten Fällen sind eine Basiswährung plus die Kreuzkurs-Berechnung aus Schritt 4 einfacher und günstiger.
Nächste Schritte
Bereit, Echtzeit-Wechselkurse in Ihren Go-Dienst zu integrieren? Holen Sie sich Ihren kostenlosen Finexly-API-Key: keine Kreditkarte nötig. Starten Sie mit 1.000 kostenlosen Anfragen pro Monat und wechseln Sie bei Bedarf in einen größeren Tarif.
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 →