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
- Go 1.21 or later
- An API key from exchange-rateapi.com
- Basic familiarity with Go modules
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 →