Go Examples
Complete Go examples for integrating OpenFXRates using the standard library and popular HTTP clients.
Installation
Using Go Modules
# Initialize your module (if not already done)
go mod init your-module-name
# Install dependencies
go get github.com/joho/godotenv
For JSON parsing, Go's standard library encoding/json is sufficient.
Example 1: Get Latest Rates
Using Go's standard net/http package:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
const (
APIKey = "your-api-key-here"
BaseURL = "https://api.openfxrates.com"
)
type LatestRatesResponse struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
func getLatestRates(base, targets string) (*LatestRatesResponse, error) {
// Build URL with query parameters
u, err := url.Parse(BaseURL + "/latest_rates")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("base", base)
q.Set("targets", targets)
u.RawQuery = q.Encode()
// Create request
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
// Set headers
req.Header.Set("X-API-Key", APIKey)
req.Header.Set("Content-Type", "application/json")
// Make request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Check status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %s - %s", resp.Status, string(body))
}
// Parse JSON
var data LatestRatesResponse
if err := json.Unmarshal(body, &data); err != nil {
return nil, err
}
// Display results
fmt.Printf("Base: %s\n", data.Base)
fmt.Printf("Date: %s\n", data.Date)
fmt.Println("Rates:")
for currency, rate := range data.Rates {
fmt.Printf(" %s: %.4f\n", currency, rate)
}
return &data, nil
}
func main() {
if _, err := getLatestRates("USD", "EUR,GBP,JPY"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Example 2: Convert Currency
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type ConversionResponse struct {
From string `json:"from"`
Amount float64 `json:"amount"`
Conversions map[string]float64 `json:"conversions"`
}
func convertCurrency(from, to string, amount float64) (*ConversionResponse, error) {
// Build URL
u, err := url.Parse(BaseURL + "/convert")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("from", from)
q.Set("to", to)
q.Set("amount", fmt.Sprintf("%.2f", amount))
u.RawQuery = q.Encode()
// Create request
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", APIKey)
// Make request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read and parse response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %s", string(body))
}
var data ConversionResponse
if err := json.Unmarshal(body, &data); err != nil {
return nil, err
}
// Display results
fmt.Printf("Converting: %.2f %s\n", data.Amount, data.From)
fmt.Println("Results:")
for currency, converted := range data.Conversions {
fmt.Printf(" %s: %.2f\n", currency, converted)
}
return &data, nil
}
func main() {
if _, err := convertCurrency("USD", "EUR,GBP,JPY", 100.0); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Example 3: Get Historical Rates
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type HistoricalRatesResponse struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
func getHistoricalRates(base, date, targets string) (*HistoricalRatesResponse, error) {
u, err := url.Parse(BaseURL + "/historical_rates")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("base", base)
q.Set("date", date)
q.Set("targets", targets)
u.RawQuery = q.Encode()
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", APIKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %s", string(body))
}
var data HistoricalRatesResponse
if err := json.Unmarshal(body, &data); err != nil {
return nil, err
}
fmt.Printf("Historical rates for %s\n", data.Date)
fmt.Printf("Base: %s\n", data.Base)
fmt.Println("Rates:")
for currency, rate := range data.Rates {
fmt.Printf(" %s: %.4f\n", currency, rate)
}
return &data, nil
}
func main() {
// Get rates from January 15, 2024
if _, err := getHistoricalRates("USD", "2024-01-15", "EUR,GBP"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Example 4: List All Currencies
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type Currency struct {
Code string `json:"code"`
Name string `json:"name"`
Symbol *string `json:"symbol,omitempty"`
}
type CurrenciesResponse struct {
Currencies []Currency `json:"currencies"`
}
func listCurrencies() ([]Currency, error) {
req, err := http.NewRequest("GET", BaseURL+"/currencies", nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", APIKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %s", string(body))
}
var data CurrenciesResponse
if err := json.Unmarshal(body, &data); err != nil {
return nil, err
}
fmt.Printf("Available currencies: %d\n\n", len(data.Currencies))
for _, currency := range data.Currencies {
symbol := ""
if currency.Symbol != nil {
symbol = fmt.Sprintf(" (%s)", *currency.Symbol)
}
fmt.Printf("%s - %s%s\n", currency.Code, currency.Name, symbol)
}
return data.Currencies, nil
}
func main() {
if _, err := listCurrencies(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Example 5: API Client Package
Complete reusable client package:
package openfxrates
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
type Client struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
type LatestRatesResponse struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
type ConversionResponse struct {
From string `json:"from"`
Amount float64 `json:"amount"`
Conversions map[string]float64 `json:"conversions"`
}
type HistoricalRatesResponse struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
type Currency struct {
Code string `json:"code"`
Name string `json:"name"`
Symbol *string `json:"symbol,omitempty"`
}
type CurrenciesResponse struct {
Currencies []Currency `json:"currencies"`
}
type ErrorResponse struct {
Message string `json:"message"`
Code string `json:"code,omitempty"`
}
// NewClient creates a new OpenFXRates API client
func NewClient(apiKey string) *Client {
return &Client{
APIKey: apiKey,
BaseURL: "https://api.openfxrates.com",
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// NewClientFromEnv creates a client using environment variables
func NewClientFromEnv() (*Client, error) {
apiKey := os.Getenv("OPENFXRATES_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("OPENFXRATES_API_KEY environment variable not set")
}
return NewClient(apiKey), nil
}
// GetLatestRates fetches the latest exchange rates
func (c *Client) GetLatestRates(base, targets string) (*LatestRatesResponse, error) {
u, err := url.Parse(c.BaseURL + "/latest_rates")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("base", base)
if targets != "" {
q.Set("targets", targets)
}
u.RawQuery = q.Encode()
var response LatestRatesResponse
if err := c.makeRequest("GET", u.String(), &response); err != nil {
return nil, err
}
return &response, nil
}
// GetHistoricalRates fetches historical exchange rates
func (c *Client) GetHistoricalRates(base, date, targets string) (*HistoricalRatesResponse, error) {
u, err := url.Parse(c.BaseURL + "/historical_rates")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("base", base)
q.Set("date", date)
if targets != "" {
q.Set("targets", targets)
}
u.RawQuery = q.Encode()
var response HistoricalRatesResponse
if err := c.makeRequest("GET", u.String(), &response); err != nil {
return nil, err
}
return &response, nil
}
// ConvertCurrency converts an amount from one currency to others
func (c *Client) ConvertCurrency(from, to string, amount float64) (*ConversionResponse, error) {
u, err := url.Parse(c.BaseURL + "/convert")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("from", from)
q.Set("to", to)
q.Set("amount", fmt.Sprintf("%.2f", amount))
u.RawQuery = q.Encode()
var response ConversionResponse
if err := c.makeRequest("GET", u.String(), &response); err != nil {
return nil, err
}
return &response, nil
}
// ListCurrencies fetches all available currencies
func (c *Client) ListCurrencies() ([]Currency, error) {
var response CurrenciesResponse
if err := c.makeRequest("GET", c.BaseURL+"/currencies", &response); err != nil {
return nil, err
}
return response.Currencies, nil
}
// makeRequest performs an HTTP request and handles the response
func (c *Client) makeRequest(method, url string, result interface{}) error {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return err
}
req.Header.Set("X-API-Key", c.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(body, &errResp); err != nil {
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
}
return fmt.Errorf("API error %d: %s", resp.StatusCode, errResp.Message)
}
return json.Unmarshal(body, result)
}
Using the Client Package
package main
import (
"fmt"
"log"
"os"
"your-module/openfxrates"
)
func main() {
// Create client from environment variable
client, err := openfxrates.NewClientFromEnv()
if err != nil {
log.Fatal(err)
}
// Get latest rates
latest, err := client.GetLatestRates("USD", "EUR,GBP,JPY")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Base: %s, Date: %s\n", latest.Base, latest.Date)
fmt.Printf("Rates: %v\n\n", latest.Rates)
// Convert currency
conversion, err := client.ConvertCurrency("USD", "EUR", 100.0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Conversions: %v\n\n", conversion.Conversions)
// Get historical rates
historical, err := client.GetHistoricalRates("USD", "2024-01-15", "EUR,GBP")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Historical rates for %s: %v\n\n", historical.Date, historical.Rates)
// List currencies
currencies, err := client.ListCurrencies()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total currencies: %d\n", len(currencies))
}
Example 6: Concurrent Requests
Using goroutines for concurrent API calls:
package main
import (
"fmt"
"sync"
)
func fetchMultipleCurrencies() error {
client := NewClient(APIKey)
bases := []string{"USD", "EUR", "GBP"}
results := make(map[string]*LatestRatesResponse)
errors := make(map[string]error)
var wg sync.WaitGroup
var mu sync.Mutex
for _, base := range bases {
wg.Add(1)
go func(b string) {
defer wg.Done()
data, err := client.GetLatestRates(b, "EUR,GBP,JPY")
mu.Lock()
defer mu.Unlock()
if err != nil {
errors[b] = err
} else {
results[b] = data
}
}(base)
}
wg.Wait()
// Display results
for base, data := range results {
fmt.Printf("\n%s Rates:\n", base)
for currency, rate := range data.Rates {
fmt.Printf(" %s: %.4f\n", currency, rate)
}
}
// Display errors
for base, err := range errors {
fmt.Printf("\nError fetching %s rates: %v\n", base, err)
}
return nil
}
func main() {
if err := fetchMultipleCurrencies(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Example 7: Error Handling with Custom Types
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
var (
ErrInvalidAPIKey = errors.New("invalid API key")
ErrRateLimitExceeded = errors.New("rate limit exceeded")
ErrNotFound = errors.New("resource not found")
)
type APIError struct {
StatusCode int
Message string
Code string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message)
}
func makeRequestWithErrorHandling(method, url, apiKey string, result interface{}) error {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return err
}
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// Handle different status codes
switch resp.StatusCode {
case http.StatusOK:
return json.Unmarshal(body, result)
case http.StatusUnauthorized:
return ErrInvalidAPIKey
case http.StatusTooManyRequests:
return ErrRateLimitExceeded
case http.StatusNotFound:
return ErrNotFound
default:
var errResp struct {
Message string `json:"message"`
Code string `json:"code"`
}
json.Unmarshal(body, &errResp)
return &APIError{
StatusCode: resp.StatusCode,
Message: errResp.Message,
Code: errResp.Code,
}
}
}
func main() {
var data LatestRatesResponse
err := makeRequestWithErrorHandling("GET",
"https://api.openfxrates.com/latest_rates?base=USD&targets=EUR",
APIKey, &data)
if err != nil {
switch {
case errors.Is(err, ErrInvalidAPIKey):
fmt.Println("Error: Invalid API key. Please check your credentials.")
case errors.Is(err, ErrRateLimitExceeded):
fmt.Println("Error: Rate limit exceeded. Please try again later.")
case errors.Is(err, ErrNotFound):
fmt.Println("Error: Resource not found.")
default:
fmt.Printf("Error: %v\n", err)
}
os.Exit(1)
}
fmt.Printf("Success: %s rates\n", data.Base)
}
Example 8: Retry Logic with Exponential Backoff
package main
import (
"fmt"
"math"
"time"
)
func retryWithBackoff(maxRetries int, fn func() error) error {
var err error
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
if i < maxRetries-1 {
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
fmt.Printf("Request failed, retrying in %v... (attempt %d/%d)\n",
waitTime, i+1, maxRetries)
time.Sleep(waitTime)
}
}
return fmt.Errorf("max retries exceeded: %w", err)
}
func main() {
client := NewClient(APIKey)
err := retryWithBackoff(3, func() error {
_, err := client.GetLatestRates("USD", "EUR,GBP,JPY")
return err
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("Success!")
}
Best Practices
1. Environment Variables
Use the godotenv package to manage environment variables:
package main
import (
"log"
"os"
"github.com/joho/godotenv"
)
func init() {
// Load .env file
if err := godotenv.Load(); err != nil {
log.Println("No .env file found")
}
}
func main() {
apiKey := os.Getenv("OPENFXRATES_API_KEY")
if apiKey == "" {
log.Fatal("OPENFXRATES_API_KEY not set")
}
client := NewClient(apiKey)
// Use client...
}
Create a .env file:
OPENFXRATES_API_KEY=your-api-key-here
2. Context for Cancellation
Use context for request cancellation and timeouts:
import (
"context"
"net/http"
"time"
)
func (c *Client) GetLatestRatesWithContext(ctx context.Context, base, targets string) (*LatestRatesResponse, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/latest_rates", nil)
if err != nil {
return nil, err
}
// Set headers and query params...
resp, err := c.HTTPClient.Do(req)
// Handle response...
}
// Usage with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
data, err := client.GetLatestRatesWithContext(ctx, "USD", "EUR")
3. Connection Pooling
Configure HTTP client for connection pooling:
import (
"net"
"net/http"
"time"
)
func NewOptimizedClient(apiKey string) *Client {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
return &Client{
APIKey: apiKey,
BaseURL: "https://api.openfxrates.com",
HTTPClient: &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
},
}
}
4. Response Caching
Simple in-memory cache implementation:
import (
"sync"
"time"
)
type CachedClient struct {
client *Client
cache map[string]cacheEntry
mu sync.RWMutex
ttl time.Duration
}
type cacheEntry struct {
data interface{}
timestamp time.Time
}
func NewCachedClient(apiKey string, ttl time.Duration) *CachedClient {
return &CachedClient{
client: NewClient(apiKey),
cache: make(map[string]cacheEntry),
ttl: ttl,
}
}
func (c *CachedClient) GetLatestRatesCached(base, targets string) (*LatestRatesResponse, error) {
cacheKey := fmt.Sprintf("%s:%s", base, targets)
// Check cache
c.mu.RLock()
if entry, found := c.cache[cacheKey]; found {
if time.Since(entry.timestamp) < c.ttl {
c.mu.RUnlock()
return entry.data.(*LatestRatesResponse), nil
}
}
c.mu.RUnlock()
// Fetch fresh data
data, err := c.client.GetLatestRates(base, targets)
if err != nil {
return nil, err
}
// Update cache
c.mu.Lock()
c.cache[cacheKey] = cacheEntry{
data: data,
timestamp: time.Now(),
}
c.mu.Unlock()
return data, nil
}