Zurück zum Blog

Wechselkurs-API in Go: Produktionsreifes Golang-Tutorial (2026)

V
Vlado Grigirov
November 10, 2026
Currency API Golang Tutorial Exchange Rates Finexly Backend

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.Client mit Nullwerten hat kein Timeout, sodass eine hängende Verbindung eine Goroutine für immer festhalten kann. Wir setzen 5 * time.Second und 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.Error die vollständige URL samt Schlüssel in der Fehlermeldung. Wir packen ihn mit errors.As aus und behalten nur den zugrunde liegenden Fehler. Diesen Fehler zu loggen ist damit unbedenklich.
  • Ein typisierter APIError. Aufrufer können mit errors.As anhand von Status verzweigen, etwa bei 429 zurückfahren oder bei 403 alarmieren. Der Body wird über io.LimitReader gelesen, 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:

  1. Leser werden nie durch eine Aktualisierung blockiert. Der Netzwerkaufruf findet außerhalb der Lese-/Schreibsperre statt.
  2. 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.
  3. Veraltete Daten sind besser als gar keine. Schlägt die Aktualisierung fehl und liegt ein älterer Snapshot vor, geben wir ihn mit stale=true zurü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 429 mit 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.

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 →

Diesen Artikel teilen