Go's speed, simplicity, and excellent concurrency primitives make it a natural fit for building financial microservices. Whether you are running a payments gateway, a pricing engine, or a data pipeline, integrating an exchange rate API in Go is fast and produces code that is easy to maintain. This guide covers everything from basic HTTP calls to concurrent multi-currency fetches and production-ready API handlers.

Why Use an Exchange Rate API in Go?

Go services often sit on the critical path of financial transactions. You need exchange rate data that is accurate, fresh, and retrieved with minimal latency. An exchange rate API in Go lets you pull live rates for 160+ currencies with a single HTTP call, avoiding the overhead of maintaining your own rate database. The Exchange Rate API returns lightweight JSON, authenticates via bearer tokens, and offers a free tier with 1,500 monthly requests.

Prerequisites

Basic HTTP Request with net/http

Go's standard library is all you need. No third-party HTTP client required.


    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"io"
    	"log"
    	"net/http"
    	"net/url"
    )
    
    const (
    	baseURL = "https://api.allratestoday.com/v1"
    	apiKey  = "YOUR_API_KEY"
    )
    
    type LatestRatesResponse struct {
    	Base  string             `json:"base"`
    	Date  string             `json:"date"`
    	Rates map[string]float64 `json:"rates"`
    }
    
    func getLatestRates(base string) (*LatestRatesResponse, error) {
    	u, _ := url.Parse(baseURL + "/latest")
    	q := u.Query()
    	q.Set("base", base)
    	u.RawQuery = q.Encode()
    
    	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
    	if err != nil {
    		return nil, fmt.Errorf("creating request: %w", err)
    	}
    
    	req.Header.Set("Authorization", "Bearer "+apiKey)
    	req.Header.Set("Accept", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		return nil, fmt.Errorf("executing request: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode != http.StatusOK {
    		body, _ := io.ReadAll(resp.Body)
    		return nil, fmt.Errorf("API returned %d: %s", resp.StatusCode, body)
    	}
    
    	var result LatestRatesResponse
    	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    		return nil, fmt.Errorf("decoding response: %w", err)
    	}
    
    	return &result, nil
    }
    
    func main() {
    	rates, err := getLatestRates("USD")
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	fmt.Printf("Base: %s (as of %s)\n", rates.Base, rates.Date)
    	fmt.Printf("EUR: %.4f\n", rates.Rates["EUR"])
    	fmt.Printf("GBP: %.4f\n", rates.Rates["GBP"])
    	fmt.Printf("JPY: %.4f\n", rates.Rates["JPY"])
    }
    

This is clean, idiomatic Go. The LatestRatesResponse struct maps directly to the API's JSON, and the standard net/http client handles the request.

A Complete Exchange Rate Client

For production use, wrap the API calls in a proper client struct with timeouts and reusable configuration.


    package exchangerate
    
    import (
    	"context"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"net/url"
    	"time"
    )
    
    type Client struct {
    	baseURL    string
    	apiKey     string
    	httpClient *http.Client
    }
    
    type ConversionResponse struct {
    	From   string  `json:"from"`
    	To     string  `json:"to"`
    	Amount float64 `json:"amount"`
    	Result float64 `json:"result"`
    	Rate   float64 `json:"rate"`
    }
    
    type TimeSeriesResponse struct {
    	Base      string                        `json:"base"`
    	StartDate string                        `json:"start_date"`
    	EndDate   string                        `json:"end_date"`
    	Rates     map[string]map[string]float64 `json:"rates"`
    }
    
    func NewClient(apiKey string) *Client {
    	return &Client{
    		baseURL: "https://api.allratestoday.com/v1",
    		apiKey:  apiKey,
    		httpClient: &http.Client{
    			Timeout: 10 * time.Second,
    		},
    	}
    }
    
    func (c *Client) doGet(ctx context.Context, path string, params url.Values) ([]byte, error) {
    	u, err := url.Parse(c.baseURL + path)
    	if err != nil {
    		return nil, err
    	}
    	u.RawQuery = params.Encode()
    
    	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
    	if err != nil {
    		return nil, err
    	}
    
    	req.Header.Set("Authorization", "Bearer "+c.apiKey)
    	req.Header.Set("Accept", "application/json")
    
    	resp, err := c.httpClient.Do(req)
    	if err != nil {
    		return nil, fmt.Errorf("request failed: %w", err)
    	}
    	defer resp.Body.Close()
    
    	body, err := io.ReadAll(resp.Body)
    	if err != nil {
    		return nil, fmt.Errorf("reading body: %w", err)
    	}
    
    	if resp.StatusCode != http.StatusOK {
    		return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, body)
    	}
    
    	return body, nil
    }
    
    func (c *Client) GetLatestRates(ctx context.Context, base string) (*LatestRatesResponse, error) {
    	params := url.Values{"base": {base}}
    	body, err := c.doGet(ctx, "/latest", params)
    	if err != nil {
    		return nil, err
    	}
    
    	var result LatestRatesResponse
    	return &result, json.Unmarshal(body, &result)
    }
    
    func (c *Client) Convert(ctx context.Context, from, to string, amount float64) (*ConversionResponse, error) {
    	params := url.Values{
    		"from":   {from},
    		"to":     {to},
    		"amount": {fmt.Sprintf("%.2f", amount)},
    	}
    
    	body, err := c.doGet(ctx, "/convert", params)
    	if err != nil {
    		return nil, err
    	}
    
    	var result ConversionResponse
    	return &result, json.Unmarshal(body, &result)
    }
    
    func (c *Client) GetHistoricalRates(ctx context.Context, date, base string) (*LatestRatesResponse, error) {
    	params := url.Values{"date": {date}, "base": {base}}
    	body, err := c.doGet(ctx, "/historical", params)
    	if err != nil {
    		return nil, err
    	}
    
    	var result LatestRatesResponse
    	return &result, json.Unmarshal(body, &result)
    }
    
    func (c *Client) GetTimeSeries(ctx context.Context, start, end, base string) (*TimeSeriesResponse, error) {
    	params := url.Values{
    		"start": {start},
    		"end":   {end},
    		"base":  {base},
    	}
    
    	body, err := c.doGet(ctx, "/timeseries", params)
    	if err != nil {
    		return nil, err
    	}
    
    	var result TimeSeriesResponse
    	return &result, json.Unmarshal(body, &result)
    }
    

Concurrent Multi-Currency Fetches

One of Go's greatest strengths is goroutines. When you need rates for multiple base currencies, fetch them concurrently:


    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"sync"
    	"time"
    )
    
    func fetchMultipleBases(client *exchangerate.Client, bases []string) map[string]*LatestRatesResponse {
    	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    	defer cancel()
    
    	results := make(map[string]*LatestRatesResponse)
    	var mu sync.Mutex
    	var wg sync.WaitGroup
    
    	for _, base := range bases {
    		wg.Add(1)
    		go func(b string) {
    			defer wg.Done()
    
    			rates, err := client.GetLatestRates(ctx, b)
    			if err != nil {
    				log.Printf("Error fetching %s: %v", b, err)
    				return
    			}
    
    			mu.Lock()
    			results[b] = rates
    			mu.Unlock()
    		}(base)
    	}
    
    	wg.Wait()
    	return results
    }
    
    func main() {
    	client := exchangerate.NewClient("YOUR_API_KEY")
    	bases := []string{"USD", "EUR", "GBP", "JPY", "CAD"}
    
    	start := time.Now()
    	results := fetchMultipleBases(client, bases)
    	elapsed := time.Since(start)
    
    	for base, rates := range results {
    		fmt.Printf("%s: %d currencies fetched\n", base, len(rates.Rates))
    	}
    
    	fmt.Printf("Fetched %d bases concurrently in %v\n", len(results), elapsed)
    }
    

Five API calls that would take 2-3 seconds sequentially complete in under a second when run concurrently. This is where using an exchange rate API in Go really shines.

Caching with sync.Map

To avoid redundant API calls, add an in-memory cache with TTL support:


    package cache
    
    import (
    	"sync"
    	"time"
    )
    
    type entry struct {
    	data      interface{}
    	expiresAt time.Time
    }
    
    type RateCache struct {
    	store sync.Map
    	ttl   time.Duration
    }
    
    func NewRateCache(ttl time.Duration) *RateCache {
    	return &RateCache{ttl: ttl}
    }
    
    func (c *RateCache) Get(key string) (interface{}, bool) {
    	val, ok := c.store.Load(key)
    	if !ok {
    		return nil, false
    	}
    
    	e := val.(*entry)
    	if time.Now().After(e.expiresAt) {
    		c.store.Delete(key)
    		return nil, false
    	}
    
    	return e.data, true
    }
    
    func (c *RateCache) Set(key string, data interface{}) {
    	c.store.Store(key, &entry{
    		data:      data,
    		expiresAt: time.Now().Add(c.ttl),
    	})
    }
    
    func (c *RateCache) Delete(key string) {
    	c.store.Delete(key)
    }
    

Use it in your client:


    func (c *Client) GetLatestRatesCached(ctx context.Context, base string) (*LatestRatesResponse, error) {
    	cacheKey := "latest:" + base
    
    	if cached, ok := c.cache.Get(cacheKey); ok {
    		return cached.(*LatestRatesResponse), nil
    	}
    
    	rates, err := c.GetLatestRates(ctx, base)
    	if err != nil {
    		return nil, err
    	}
    
    	c.cache.Set(cacheKey, rates)
    	return rates, nil
    }
    

Gin HTTP Handler

If you are building a REST API with Gin, expose exchange rates through your own endpoints:


    package main
    
    import (
    	"net/http"
    	"strconv"
    
    	"github.com/gin-gonic/gin"
    )
    
    func setupRouter(client *exchangerate.Client) *gin.Engine {
    	r := gin.Default()
    
    	r.GET("/api/rates", func(c *gin.Context) {
    		base := c.DefaultQuery("base", "USD")
    
    		rates, err := client.GetLatestRatesCached(c.Request.Context(), base)
    		if err != nil {
    			c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
    			return
    		}
    
    		c.JSON(http.StatusOK, rates)
    	})
    
    	r.GET("/api/convert", func(c *gin.Context) {
    		from := c.Query("from")
    		to := c.Query("to")
    		amountStr := c.Query("amount")
    
    		if from == "" || to == "" || amountStr == "" {
    			c.JSON(http.StatusBadRequest, gin.H{
    				"error": "from, to, and amount are required",
    			})
    			return
    		}
    
    		amount, err := strconv.ParseFloat(amountStr, 64)
    		if err != nil {
    			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid amount"})
    			return
    		}
    
    		result, err := client.Convert(c.Request.Context(), from, to, amount)
    		if err != nil {
    			c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
    			return
    		}
    
    		c.JSON(http.StatusOK, result)
    	})
    
    	return r
    }
    
    func main() {
    	client := exchangerate.NewClient("YOUR_API_KEY")
    	router := setupRouter(client)
    	router.Run(":8080")
    }
    

Echo HTTP Handler

If you prefer the Echo framework, the pattern is nearly identical:


    package main
    
    import (
    	"net/http"
    	"strconv"
    
    	"github.com/labstack/echo/v4"
    )
    
    func setupEchoRouter(client *exchangerate.Client) *echo.Echo {
    	e := echo.New()
    
    	e.GET("/api/rates", func(c echo.Context) error {
    		base := c.QueryParam("base")
    		if base == "" {
    			base = "USD"
    		}
    
    		rates, err := client.GetLatestRatesCached(c.Request().Context(), base)
    		if err != nil {
    			return c.JSON(http.StatusBadGateway, map[string]string{
    				"error": err.Error(),
    			})
    		}
    
    		return c.JSON(http.StatusOK, rates)
    	})
    
    	e.GET("/api/convert", func(c echo.Context) error {
    		from := c.QueryParam("from")
    		to := c.QueryParam("to")
    		amount, err := strconv.ParseFloat(c.QueryParam("amount"), 64)
    		if err != nil {
    			return c.JSON(http.StatusBadRequest, map[string]string{
    				"error": "invalid amount",
    			})
    		}
    
    		result, err := client.Convert(c.Request().Context(), from, to, amount)
    		if err != nil {
    			return c.JSON(http.StatusBadGateway, map[string]string{
    				"error": err.Error(),
    			})
    		}
    
    		return c.JSON(http.StatusOK, result)
    	})
    
    	return e
    }
    

Background Rate Refresh

For services that display rates on a dashboard, run a goroutine that refreshes the cache periodically:


    func startRateRefresher(client *exchangerate.Client, interval time.Duration) {
    	bases := []string{"USD", "EUR", "GBP"}
    
    	ticker := time.NewTicker(interval)
    	defer ticker.Stop()
    
    	// Initial fetch
    	refreshAll(client, bases)
    
    	for range ticker.C {
    		refreshAll(client, bases)
    	}
    }
    
    func refreshAll(client *exchangerate.Client, bases []string) {
    	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    	defer cancel()
    
    	for _, base := range bases {
    		client.cache.Delete("latest:" + base)
    		if _, err := client.GetLatestRatesCached(ctx, base); err != nil {
    			log.Printf("Failed to refresh %s rates: %v", base, err)
    		} else {
    			log.Printf("Refreshed %s rates", base)
    		}
    	}
    }
    

Launch it from your main function:


    go startRateRefresher(client, 1*time.Hour)
    

Testing

Go's httptest package makes it easy to test against a mock server:


    package exchangerate_test
    
    import (
    	"context"
    	"encoding/json"
    	"net/http"
    	"net/http/httptest"
    	"testing"
    )
    
    func TestGetLatestRates(t *testing.T) {
    	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		if r.URL.Path != "/latest" {
    			t.Errorf("unexpected path: %s", r.URL.Path)
    		}
    
    		if r.Header.Get("Authorization") != "Bearer test_key" {
    			t.Error("missing or invalid auth header")
    		}
    
    		json.NewEncoder(w).Encode(map[string]interface{}{
    			"base": "USD",
    			"date": "2026-05-21",
    			"rates": map[string]float64{
    				"EUR": 0.92,
    				"GBP": 0.79,
    			},
    		})
    	}))
    	defer server.Close()
    
    	client := &Client{
    		baseURL:    server.URL,
    		apiKey:     "test_key",
    		httpClient: server.Client(),
    	}
    
    	rates, err := client.GetLatestRates(context.Background(), "USD")
    	if err != nil {
    		t.Fatalf("unexpected error: %v", err)
    	}
    
    	if rates.Rates["EUR"] != 0.92 {
    		t.Errorf("expected EUR=0.92, got %f", rates.Rates["EUR"])
    	}
    }
    

Conclusion

Integrating an exchange rate API in Go produces fast, reliable services with minimal dependencies. The standard library handles HTTP and JSON natively, goroutines enable concurrent multi-currency fetches, and sync.Map provides thread-safe caching without external infrastructure. Whether you are building a microservice with Gin, Echo, or the standard net/http mux, the exchange rate API fits cleanly into Go's idioms.

The Exchange Rate API delivers 160+ currencies, sub-100ms response times, and a free tier of 1,500 requests per month. With the caching and concurrency patterns shown above, that is more than enough for most Go services.

Ready to add live exchange rates to your Go service? Sign up for a free API key at exchange-rateapi.com and visit the API documentation for the full endpoint reference.

Start Using the Exchange Rate API Today

Free tier with 1,500 requests/month. 160+ currencies updated every 60 seconds. No credit card required.

Get Your Free API Key →

Related Articles