Voltar ao Blog

API de taxas de câmbio em Go: tutorial de Golang pronto para produção (2026)

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

Se você precisa de uma API de taxas de câmbio em Go, a biblioteca padrão resolve quase tudo. Os pacotes net/http, encoding/json e context bastam para construir um cliente rápido e sem dependências. O difícil não é a chamada HTTP, e sim tudo o que a cerca: timeouts, tratamento de erros, cache, aritmética monetária e testes. A maioria dos tutoriais para em http.Get e fmt.Println, o que serve para uma demo, mas é arriscado num fluxo de pagamentos.

Neste guia você vai construir um cliente Golang pequeno, mas pensado para produção, para os endpoints da documentação da API da Finexly, envolvê-lo em um cache seguro para concorrência que sobrevive a falhas do provedor, adicionar uma função de conversão que trabalha com unidades menores inteiras e publicá-lo como serviço HTTP com testes. Tudo compila com Go 1.22 ou superior e usa apenas a biblioteca padrão.

Por que Go combina com dados de câmbio

Taxas de câmbio são lidas com muita frequência, ocupam pouco espaço e mudam devagar na escala de uma requisição web. Esse perfil combina bem com Go. Goroutines permitem atender milhares de requisições concorrentes a partir de um único snapshot em memória, um binário estático é fácil de implantar e o context.Context oferece cancelamento e prazos de primeira classe em toda chamada externa.

O objetivo do projeto é simples: seus handlers nunca devem chamar a API de taxas diretamente. Eles leem de um cache, e um único caminho seguro de atualização conversa com a API. Assim a latência fica estável, sua cota é protegida e uma instabilidade do provedor não vira uma queda no seu serviço. Para a teoria, nosso guia de cache e tratamento de erros em APIs de câmbio explica a estratégia; aqui nós a implementamos.

Passo 1: teste primeiro com cURL

Antes de escrever Go, confirme que sua chave funciona. Cadastre-se gratuitamente, copie sua chave e execute:

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.

A resposta tem três campos: base (a moeda à qual todas as taxas se referem), timestamp (segundos Unix) e rates (um mapa de código ISO 4217 para taxa). Cada valor em rates indica quantas unidades daquela moeda equivalem a uma unidade da base. Guarde a chave em uma variável de ambiente, nunca no controle de versão:

export FINEXLY_API_KEY="your_key_here"

Passo 2: um cliente tipado com timeouts e erros seguros

Crie um módulo com go mod init fxdemo e coloque o código abaixo em main.go. Ele define a struct de resposta, um APIError próprio e um Client cujo método Latest aceita um context.Context:

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
}

Alguns detalhes fazem a diferença entre uma demo e código de produção:

  • Um timeout rígido. O http.Client com valores padrão não tem timeout, então uma conexão travada pode prender uma goroutine para sempre. Definimos 5 * time.Second e também repassamos o contexto de quem chama, de modo que uma requisição cancelada pelo usuário cancela a chamada upstream.
  • Sem vazar a chave nos logs. Como a chave viaja no parâmetro apikey, o *url.Error do Go inclui a URL completa, chave e tudo, na mensagem. Nós o desembrulhamos com errors.As e mantemos apenas o erro subjacente. Registrar esse erro passa a ser seguro.
  • Um APIError tipado. Quem chama pode usar errors.As para decidir com base em Status, por exemplo recuar diante de um 429 ou alertar diante de um 403. O corpo é lido com io.LimitReader para que um servidor com defeito não faça você armazenar megabytes.
  • Um baseURL configurável. Não custa nada e torna o cliente trivial de testar, como você verá no passo 6.

Passo 3: um cache seguro para concorrência com fallback de dados antigos

Chamar a API a cada requisição consumiria sua cota e aumentaria a latência. Para a maioria das aplicações as taxas não mudam de forma relevante em poucos minutos, então faça cache. O cache abaixo tem três propriedades que exemplos simples de map com Mutex deixam de lado:

  1. Leitores nunca são bloqueados por uma atualização. A chamada de rede acontece fora do lock de leitura/escrita.
  2. Só uma goroutine atualiza por vez. Um segundo mutex, mais uma nova verificação após adquiri-lo, evita a debandada de requisições quando o TTL expira sob carga.
  3. Dado antigo é melhor do que nenhum dado. Se a atualização falhar e tivermos um snapshot anterior, devolvemos esse snapshot com stale=true para que quem chama decida o que fazer.
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
}

Repare que buscamos todas as taxas da base em uma única requisição (symbols é omitido). Uma chamada por hora atende todos os pares de moedas de que seu serviço precisa, cerca de 720 chamadas por mês para uma instância sempre ativa. Isso cabe no plano gratuito de 1.000 requisições. Um TTL de dez minutos resultaria em cerca de 4.300 chamadas por mês, então intervalos menores exigem um plano pago; veja os planos de preços.

Passo 4: converta dinheiro sem surpresas de ponto flutuante

Nunca guarde dinheiro em um float64. Guarde unidades menores inteiras (centavos, pence, ienes) e aplique a taxa uma única vez, no final, com arredondamento explícito:

// 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
}

Como as taxas são cotadas em relação a uma base, converter EUR para GBP passa por essa base: divide-se pela taxa de origem e multiplica-se pela taxa de destino. A função também trata a própria base como uma taxa exatamente igual a 1.

Há uma armadilha real. A função acima supõe que as duas moedas têm o mesmo número de casas decimais. O JPY tem 0, o USD tem 2 e o KWD tem 3, então um sistema em produção precisa escalar pelo expoente de cada moeda. Nosso guia de códigos de moeda ISO 4217 lista o expoente da unidade menor de cada código. Para uma conferência interativa rápida, use o conversor de moedas.

Passo 5: sirva tudo via HTTP

Agora ligue tudo em um handler HTTP. O handler interpreta a requisição, lê do cache usando o contexto da requisição e informa se as taxas estavam desatualizadas:

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))
}

Execute e teste a conversão de 100,00 USD para EUR (10.000 centavos):

go run . &
curl "http://localhost:8080/convert?amount=10000&from=USD&to=EUR"

A resposta inclui rates_timestamp e um indicador stale, para que os sistemas seguintes possam exibir "taxas de..." ou se recusar a cotar quando os dados estiverem velhos demais. Esse único campo costuma ser a primeira coisa que auditores e times financeiros pedem.

Passo 6: testes sem tocar na rede

Como a URL base é injetável, você pode testar toda a lógica do cliente e do cache com httptest.NewServer. Este teste prova o comportamento mais importante: quando a API começa a falhar, o cache serve dados antigos em vez de um erro.

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)
	}
}

Execute com go test ./.... Testes assim rodam em milissegundos, não exigem chave de API e podem fazer parte do CI. Você pode adicionar casos para respostas 429, JSON malformado e servidores lentos (para verificar o timeout) no mesmo estilo.

Checklist para produção

Antes de publicar, revise estes pontos:

  • Escolha um TTL sensato. Uma hora serve para faturamento e preços exibidos em lojas virtuais, e mantém uma instância dentro do plano gratuito. Funcionalidades de cotação ou trading podem exigir minutos ou segundos. Ajuste o TTL ao risco do negócio e depois escolha o plano.
  • Exponha o timestamp da taxa em qualquer lugar em que o cliente veja um valor convertido e guarde a taxa usada em cada transação para poder reproduzi-la depois.
  • Alerte sobre dados desatualizados. Emita uma métrica quando stale=true persistir além de um limite. Um cache que serve silenciosamente dados de um dia atrás é pior do que um erro.
  • Aqueça o cache na inicialização e falhe as verificações de prontidão se a primeira busca falhar, para que uma chave inválida seja descoberta no deploy.
  • Trate o 429 com espera progressiva em vez de tentar de novo imediatamente. Se isso ocorre com frequência, seu TTL é curto demais ou há várias instâncias atualizando de forma independente. Um cache compartilhado, como o Redis, resolve o segundo caso.
  • Compare provedores de vez em quando. Se você está avaliando fontes de dados, nossa página para comparar APIs de câmbio e a visão geral da API de câmbio gratuita mostram o que observar.

Se você trabalha com mais de uma linguagem, os mesmos padrões aparecem em nosso tutorial de Rust e no tutorial de Python.

Perguntas frequentes

Existe uma API de taxas de câmbio gratuita para Go?

Sim. A Finexly tem um plano gratuito com 1.000 requisições por mês, e o cliente acima não precisa de pacotes de terceiros. Com cache de uma hora, uma instância sempre ativa usa cerca de 720 requisições por mês, o que deixa folga. Lembre-se de que cada instância extra atualiza por conta própria. Veja os planos de preços para os limites atuais.

Devo usar float64 para taxas de câmbio em Go?

Usar float64 para a taxa costuma ser aceitável, já que taxas são aproximações com poucos dígitos significativos. Usá-lo para valores monetários não é. Guarde os valores como unidades menores inteiras, ou use uma biblioteca decimal, e arredonde uma única vez no final.

Com que frequência devo atualizar as taxas de câmbio?

Depende da sua tolerância a risco. Para preços exibidos e relatórios, de 15 a 60 minutos costuma bastar. O faturamento normalmente trava uma taxa por documento. Tudo o que movimenta dinheiro em tempo real deve guardar a taxa exata e o timestamp usados, seja qual for o intervalo de atualização.

Como evito que minha API key vaze nos logs do Go?

Quando a chave é enviada como parâmetro de consulta, os erros retornados por http.Client embutem a URL completa. Desembrulhe o *url.Error com errors.As e registre apenas o campo Err, como faz o cliente deste guia. Evite também registrar URLs completas em middlewares.

Posso buscar várias moedas base de forma concorrente?

Sim. Use golang.org/x/sync/errgroup com SetLimit para limitar o paralelismo e compartilhe o mesmo Client, que é seguro para uso concorrente. Na maioria dos casos, uma única moeda base mais o cálculo de taxas cruzadas do passo 4 é mais simples e barato.

Próximos passos

Pronto para integrar taxas de câmbio em tempo real ao seu serviço em Go? Obtenha sua API key gratuita da Finexly: sem cartão de crédito. Comece com 1.000 requisições gratuitas por mês e faça upgrade conforme crescer.

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 →

Compartilhar este artigo