Go ile döviz kuru API'si kullanmak istiyorsanız standart kütüphane neredeyse yeterlidir. net/http, encoding/json ve context paketleri, hızlı ve bağımlılıksız bir istemci yazmak için yeterlidir. Zor olan HTTP çağrısının kendisi değil, onu çevreleyen her şeydir: zaman aşımları, hata yönetimi, önbellekleme, para aritmetiği ve testler. Çoğu eğitim http.Get ve fmt.Println ile biter. Bu bir demo için uygundur, ancak bir ödeme akışında risklidir.
Bu rehberde Finexly API belgelerindeki uç noktalar için küçük ama üretim odaklı bir Golang istemcisi geliştirecek, onu sağlayıcı kesintilerinden etkilenmeyen eşzamanlılık güvenli bir önbelleğe saracak, tam sayı alt birimlerle çalışan bir dönüştürme fonksiyonu ekleyecek ve testlerle birlikte bir HTTP servisi olarak yayınlayacaksınız. Tüm kod Go 1.22 veya daha yenisiyle derlenir ve yalnızca standart kütüphaneyi kullanır.
Go Neden Döviz Kuru Verilerine Uygundur
Döviz kurları çok sık okunur, küçüktür ve bir web isteği ölçeğinde yavaş değişir. Bu profil Go'ya çok uygundur. Goroutine'ler tek bir bellek içi anlık görüntüden binlerce eşzamanlı isteği ucuza karşılamayı sağlar, tek bir statik ikili dosya dağıtması kolaydır ve context.Context her giden çağrı için birinci sınıf iptal ve son tarih desteği sunar.
Tasarım hedefi basittir: istek işleyicileriniz kur API'sini asla doğrudan çağırmamalıdır. Onlar önbellekten okur, API ile yalnızca tek ve güvenli bir yenileme yolu konuşur. Böylece gecikme sabit kalır, kotanız korunur ve sağlayıcıdaki bir aksama sizin kesintinize dönüşmez. Arkasındaki kuram için döviz API'lerinde önbellekleme ve hata yönetimi rehberimize bakabilirsiniz; burada bunu uyguluyoruz.
Adım 1: Önce cURL ile Deneyin
Go yazmadan önce anahtarınızın çalıştığını doğrulayın. Ücretsiz kaydolun, anahtarınızı kopyalayın ve şunu çalıştırın:
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.
Yanıt üç alan içerir: base (tüm kurların esas alındığı para birimi), timestamp (Unix saniyesi) ve rates (ISO 4217 kodundan kura bir eşleme). rates içindeki her değer, o para biriminin kaç biriminin baz para biriminin bir birimine eşit olduğunu gösterir. Anahtarı bir ortam değişkeninde saklayın, sürüm kontrolüne asla koymayın:
export FINEXLY_API_KEY="your_key_here"Adım 2: Zaman Aşımlı ve Güvenli Hatalı Tipli İstemci
go mod init fxdemo ile bir modül oluşturun ve aşağıdaki kodu main.go içine koyun. Yanıt yapısını, özel bir APIError tipini ve Latest metodu context.Context kabul eden bir Client tanımlar:
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
}Bir demoyu üretim kodundan ayıran birkaç ayrıntı var:
- Katı bir zaman aşımı. Sıfır değerli
http.Clientiçin zaman aşımı yoktur, bu yüzden takılan tek bir bağlantı bir goroutine'i sonsuza dek tutabilir.5 * time.Secondayarlıyoruz ve çağıranın bağlamını da iletiyoruz; böylece kullanıcının iptal ettiği bir istek, yukarı akıştaki çağrıyı da iptal eder. - Günlüklere anahtar sızmaz. Anahtar
apikeysorgu parametresiyle taşındığı için Go'nun*url.Errortipi, mesajında anahtar dahil tüm URL'yi içerir. Bunuerrors.Asile açıp yalnızca altta yatan hatayı tutuyoruz. Bu hatayı günlüğe yazmak artık güvenlidir. - Tipli bir
APIError. Çağıranlarerrors.AsileStatusdeğerine göre dallanabilir; örneğin429durumunda beklemek veya403durumunda alarm vermek gibi. Gövdeio.LimitReaderile okunur, böylece hatalı çalışan bir sunucu sizi megabaytlarca veri tamponlamaya zorlayamaz. - Yapılandırılabilir bir
baseURL. Hiçbir maliyeti yoktur ve 6. adımda göreceğiniz gibi istemciyi test etmeyi çok kolaylaştırır.
Adım 3: Eski Veri Yedekli, Eşzamanlılık Güvenli Önbellek
Her istekte API'yi çağırmak kotanızı tüketir ve her yanıta gecikme ekler. Çoğu uygulamada kurlar birkaç dakika içinde anlamlı biçimde değişmez, bu yüzden önbelleğe alın. Aşağıdaki önbellek, map ile Mutex kullanan basit örneklerin gözden kaçırdığı üç özelliğe sahiptir:
- Okuyucular yenilemeyle asla engellenmez. Ağ çağrısı okuma/yazma kilidinin dışında yapılır.
- Aynı anda yalnızca bir goroutine yeniler. İkinci bir mutex ve onu aldıktan sonra yapılan yeniden kontrol, TTL yük altında dolduğunda ortaya çıkan ani istek dalgasını önler.
- Eski veri, hiç veri olmamasından iyidir. Yenileme başarısız olur ve elimizde daha eski bir anlık görüntü varsa, onu
stale=truebayrağıyla döndürürüz; ne yapılacağına çağıran karar verir.
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
}Baz para birimi için tüm kurları tek istekte aldığımıza dikkat edin (symbols verilmedi). Saatte bir çağrı, servisinizin ihtiyaç duyduğu her döviz çiftini karşılar; bu da sürekli çalışan tek bir örnek için ayda yaklaşık 720 çağrıdır. Bu, 1.000 istekli ücretsiz plana sığar. On dakikalık bir TTL ayda yaklaşık 4.300 çağrı demektir, dolayısıyla daha kısa yenileme aralıkları ücretli plan gerektirir; fiyat planlarına bakın.
Adım 4: Kayan Noktalı Sürprizler Olmadan Para Dönüştürme
Parayı asla float64 içinde saklamayın. Tam sayı alt birimleri (sent, peni, yen) saklayın ve kuru yalnızca sonda, açık yuvarlamayla bir kez uygulayın:
// 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
}Kurlar bir baza göre verildiği için EUR'dan GBP'ye dönüşüm bu baz üzerinden geçer: kaynak kura bölünür, hedef kurla çarpılır. Fonksiyon, bazın kendisini de tam olarak 1 olan bir kur sayar.
Gerçek bir tuzak var. Yukarıdaki fonksiyon iki para biriminin de aynı sayıda ondalık basamağa sahip olduğunu varsayar. JPY'de 0, USD'de 2, KWD'de 3 basamak vardır; bu yüzden üretim sisteminin her para biriminin üssüne göre ölçeklemesi gerekir. ISO 4217 para birimi kodları rehberimiz her kod için alt birim üssünü listeler. Sayılarınızı hızlıca etkileşimli olarak denetlemek için döviz çeviriciyi kullanın.
Adım 5: HTTP Üzerinden Sunun
Şimdi her şeyi bir HTTP işleyicisine bağlayın. İşleyici isteği ayrıştırır, istek bağlamıyla önbellekten okur ve kurların eski olup olmadığını bildirir:
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))
}Çalıştırın ve 100,00 USD'yi EUR'ya (10.000 sent) çevirmeyi deneyin:
go run . &
curl "http://localhost:8080/convert?amount=10000&from=USD&to=EUR"Yanıt rates_timestamp ve bir stale bayrağı içerir; böylece sonraki sistemler "kurlar şu tarihli..." gösterebilir veya veri çok eskiyse fiyat vermeyi reddedebilir. Denetçilerin ve finans ekiplerinin ilk istediği şey çoğu zaman bu tek alandır.
Adım 6: Ağa Dokunmadan Test Edin
Baz URL enjekte edilebildiği için istemci ve önbellek mantığının tamamını httptest.NewServer ile test edebilirsiniz. Bu test en önemli davranışı kanıtlar: API başarısız olmaya başladığında önbellek hata yerine eski veriyi sunar.
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)
}
}go test ./... ile çalıştırın. Bunun gibi testler milisaniyeler içinde çalışır, API anahtarı gerektirmez ve CI'a eklenebilir. Aynı tarzda 429 yanıtları, bozuk JSON ve yavaş sunucular (zaman aşımını doğrulamak için) için de durumlar ekleyebilirsiniz.
Üretim Kontrol Listesi
Yayına almadan önce şu noktaları gözden geçirin:
- Makul bir TTL seçin. Bir saat faturalama ve e-ticaret gösterim fiyatları için uygundur ve tek bir örneği ücretsiz planda tutar. Fiyat teklifi veya alım satım özellikleri dakikalar ya da saniyeler gerektirebilir. TTL'yi iş riskine göre belirleyin, ardından planı seçin.
- Kur zaman damgasını gösterin. Müşterinin dönüştürülmüş bir tutar gördüğü her yerde bunu gösterin ve sonradan yeniden üretilebilsin diye her işlemle birlikte kullandığınız kuru saklayın.
- Eskimeye karşı alarm kurun.
stale=truebir eşiğin ötesinde sürerse bir metrik yayınlayın. Sessizce bir günlük veri sunan önbellek, bir hatadan daha kötüdür. - Önbelleği başlangıçta ısıtın ve ilk alma başarısız olursa hazırlık kontrollerini başarısız kılın; yanlış anahtar dağıtım sırasında yakalanır.
429durumunu geri çekilmeyle yönetin, hemen yeniden denemeyin. Sık görüyorsanız TTL'niz çok kısadır ya da birden çok örnek bağımsız yeniliyordur. Redis gibi paylaşımlı bir önbellek ikinci durumu çözer.- Sağlayıcıları ara sıra karşılaştırın. Veri kaynaklarını değerlendiriyorsanız döviz API'lerini karşılaştırın sayfamız ve ücretsiz döviz API'si genel bakışımız nelere bakmanız gerektiğini gösterir.
Birden fazla dille çalışıyorsanız aynı kalıplar Rust eğitimimizde ve Python eğitimimizde da yer alır.
Sıkça Sorulan Sorular
Go için ücretsiz bir döviz kuru API'si var mı?
Evet. Finexly'nin ayda 1.000 istek içeren ücretsiz bir planı vardır ve yukarıdaki istemci üçüncü taraf paket gerektirmez. Bir saatlik önbellekle, sürekli çalışan tek bir örnek ayda yaklaşık 720 istek kullanır ve pay bırakır. Her ek örneğin kendi başına yenilediğini unutmayın. Güncel sınırlar için fiyat planlarına bakın.
Go'da döviz kurları için float64 kullanmalı mıyım?
Kur için float64 kullanmak genellikle kabul edilebilir, çünkü kurlar birkaç anlamlı basamaklı yaklaşık değerlerdir. Para tutarları için ise kabul edilemez. Tutarları tam sayı alt birimler olarak saklayın veya bir ondalık kütüphane kullanın ve yalnızca sonda bir kez yuvarlayın.
Döviz kurlarını ne sıklıkla yenilemeliyim?
Risk toleransınıza bağlıdır. Gösterim fiyatları ve raporlar için genellikle 15 ila 60 dakika yeterlidir. Faturalama çoğunlukla belge başına bir kuru sabitler. Gerçek zamanlı para hareketi yapan her şey, yenileme aralığı ne olursa olsun kullanılan tam kuru ve zaman damgasını saklamalıdır.
API anahtarımın Go günlüklerine sızmasını nasıl önlerim?
Anahtar sorgu parametresi olarak gönderildiğinde, http.Client hataları tam URL'yi içerir. Bu rehberdeki istemcinin yaptığı gibi *url.Error değerini errors.As ile açın ve yalnızca Err alanını günlüğe yazın. Ayrıca ara katmanlarda tam istek URL'lerini günlüğe yazmaktan kaçının.
Birden çok baz para birimini eşzamanlı alabilir miyim?
Evet. Paralelliği sınırlamak için SetLimit ile golang.org/x/sync/errgroup kullanın ve eşzamanlı kullanım için güvenli olan aynı Client değerini paylaşın. Çoğu durumda tek bir baz para birimi ve 4. adımdaki çapraz kur hesabı daha basit ve daha ucuzdur.
Sonraki Adımlar
Gerçek zamanlı döviz kurlarını Go servisinize entegre etmeye hazır mısınız? Ücretsiz Finexly API anahtarınızı alın: kredi kartı gerekmez. Ayda 1.000 ücretsiz istekle başlayın ve büyüdükçe yükseltin.
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 →