Circuit breaker em Go é um padrão simples para um problema caro: impedir que uma dependência instável derrube o seu serviço por arrasto. Quando uma API de pagamento, antifraude, frete, nota fiscal, CRM ou autenticação começa a falhar, o pior comportamento é continuar abrindo chamadas sem limite, segurando goroutines, ocupando conexões e aumentando a latência de todos os usuários. O breaker corta o caminho antes que a falha vire efeito dominó.
Em português direto: circuit breaker é um disjuntor lógico. Enquanto a dependência responde bem, as chamadas passam. Quando os erros passam de um limite, o circuito abre e novas chamadas falham rápido. Depois de um tempo, ele deixa poucas tentativas passarem no modo half-open. Se a dependência se recuperou, fecha de novo. Se continua ruim, mantém o corte.
Este guia mostra como aplicar circuit breaker em Go para clients HTTP, com context.Context, timeouts, fallback, métricas, logs, testes e cuidados de produção. Ele complementa context, timeout e cancelamento, httptrace para debug de cliente HTTP, rate limiting em Go, OpenTelemetry em Go e webhooks com idempotência.
Quando usar circuit breaker
Use circuit breaker quando a sua aplicação depende de algo que pode ficar lento, instável ou indisponível por alguns minutos: API externa, gateway de pagamento, serviço interno, banco secundário, cache remoto ou fornecedor B2B. O objetivo não é esconder erro. É proteger capacidade, degradar com clareza e dar tempo para a dependência se recuperar.
Bons sinais de que o padrão faz sentido:
- chamadas HTTP externas aparecem no caminho crítico do request;
- timeouts de fornecedor já causaram picos de latência;
- retries aumentam a carga em vez de resolver;
- dashboards mostram cascata de erro entre serviços;
- usuários poderiam receber uma resposta parcial, fila ou mensagem de indisponibilidade controlada;
- o time precisa de métrica clara de dependência aberta, fechada e em recuperação.
Não use breaker para mascarar bug determinístico do seu próprio código. Se toda chamada falha por payload inválido, credencial errada ou contrato quebrado, o conserto é validação e deploy, não disjuntor. Também não use como substituto para timeout. Uma chamada sem timeout pode prender o worker antes mesmo de o breaker contabilizar erro.
Estados: closed, open e half-open
O breaker opera em três estados:
- Closed: funcionamento normal. As chamadas passam e o breaker contabiliza sucesso, erro e timeout.
- Open: a taxa ou sequência de falhas passou do limite. O breaker falha rápido sem chamar a dependência.
- Half-open: depois de uma janela de espera, poucas chamadas de teste são permitidas. Se der certo, fecha. Se falhar, abre novamente.
Esse desenho é melhor que retry infinito. Retry pode ser útil para erro transitório, mas aumenta tráfego justamente quando o fornecedor está sob pressão. Circuit breaker reduz o volume, protege seus recursos e deixa o incidente mais previsível.
Um client HTTP com breaker
A forma mais comum em Go é encapsular o client externo em um tipo próprio. Assim, handler, worker e job não precisam saber se a proteção vem de biblioteca, métrica ou regra local. O exemplo abaixo usa github.com/sony/gobreaker, uma biblioteca pequena e conhecida no ecossistema Go.
go get github.com/sony/gobreaker
package antifraude
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/sony/gobreaker"
)
type Client struct {
baseURL string
http *http.Client
breaker *gobreaker.CircuitBreaker
}
type Request struct {
OrderID string `json:"order_id"`
Amount int64 `json:"amount"`
}
type Response struct {
Approved bool `json:"approved"`
Reason string `json:"reason"`
}
func New(baseURL string) *Client {
st := gobreaker.Settings{
Name: "antifraude-http",
MaxRequests: 3,
Interval: 1 * time.Minute,
Timeout: 30 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.50
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
// Troque por slog, métrica ou evento de observabilidade.
fmt.Printf("breaker %s mudou de %s para %s\n", name, from, to)
},
}
return &Client{
baseURL: baseURL,
http: &http.Client{
Timeout: 5 * time.Second,
},
breaker: gobreaker.NewCircuitBreaker(st),
}
}
func (c *Client) Analyze(ctx context.Context, req Request) (Response, error) {
result, err := c.breaker.Execute(func() (any, error) {
return c.call(ctx, req)
})
if err != nil {
return Response{}, err
}
return result.(Response), nil
}
func (c *Client) call(ctx context.Context, payload Request) (Response, error) {
body, err := json.Marshal(payload)
if err != nil {
return Response{}, err
}
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
c.baseURL+"/v1/analyze",
bytes.NewReader(body),
)
if err != nil {
return Response{}, err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(httpReq)
if err != nil {
return Response{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests {
return Response{}, fmt.Errorf("antifraude indisponível: status %d", resp.StatusCode)
}
if resp.StatusCode >= 400 {
return Response{}, fmt.Errorf("requisição inválida para antifraude: status %d", resp.StatusCode)
}
var out Response
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return Response{}, err
}
return out, nil
}
Repare em três decisões importantes. Primeiro, o http.Client tem timeout. Segundo, a chamada recebe ctx de fora, então cancelamento do request, worker ou job continua valendo. Terceiro, nem todo status HTTP deve contar igual. Erros 500 e 429 indicam indisponibilidade ou sobrecarga da dependência. Erros 400 geralmente indicam problema no seu payload; eles devem ir para log e correção, mas não necessariamente abrir o circuito.
Fallback sem mentir para o usuário
Quando o breaker abre, você precisa decidir como degradar. Fallback bom é honesto. Ele não inventa aprovação de pagamento, não ignora regra de segurança e não marca uma operação crítica como concluída sem evidência. Em alguns sistemas, o fallback correto é negar temporariamente. Em outros, é aceitar a entrada, colocar em fila e informar que a análise será concluída depois.
func Handler(client *antifraude.Client, queue Queue) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
result, err := client.Analyze(ctx, antifraude.Request{
OrderID: "pedido-123",
Amount: 12990,
})
if err != nil {
if enqueueErr := queue.EnqueueReview(r.Context(), "pedido-123"); enqueueErr != nil {
http.Error(w, "serviço temporariamente indisponível", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte("pedido recebido para análise assíncrona"))
return
}
if !result.Approved {
http.Error(w, "pedido em revisão", http.StatusAccepted)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pedido aprovado"))
}
}
Esse exemplo transforma uma falha externa em processamento assíncrono. Para isso funcionar de verdade, a fila precisa ser idempotente, observável e segura contra duplicidade. Se você ainda não tem esse desenho, leia idempotência, retry e DLQ em Go antes de colocar fallback em produção.
Configuração que não vira chute
Os parâmetros do breaker devem refletir o comportamento normal da dependência. Um serviço que recebe dez chamadas por minuto não deve abrir com a mesma regra de um serviço que recebe dez mil chamadas por minuto. Comece conservador:
http.Client.Timeout: menor que o orçamento do request;ReadyToTrip: exige volume mínimo antes de calcular taxa de falha;Timeoutdo breaker: tempo suficiente para a dependência respirar;MaxRequests: poucas tentativas no half-open para evitar enxurrada;Interval: janela para limpar contadores quando o sistema está saudável.
Uma regra prática: se o endpoint é crítico e a dependência é instável, prefira falhar rápido com mensagem clara a segurar goroutine até estourar timeout. Se o endpoint é assíncrono, empurre para fila e responda 202 Accepted. Se é uma consulta opcional, mostre conteúdo principal sem o bloco dependente.
Métricas e logs obrigatórios
Circuit breaker sem observabilidade vira superstição. Você precisa saber quando ele abriu, por quê, por quanto tempo e qual foi o impacto no usuário. No mínimo, registre:
- estado atual do breaker por dependência;
- mudanças de estado com
frometo; - total de chamadas permitidas, falhas rápidas e chamadas reais;
- status HTTP e tipo de erro da dependência;
- latência antes do erro;
- número de fallbacks executados;
- volume de operações mandadas para fila.
Com slog em Go, um evento de mudança de estado pode carregar campos estáveis:
logger.Warn("circuit breaker mudou de estado",
"name", name,
"from", from.String(),
"to", to.String(),
)
Com OpenTelemetry em Go, adicione atributos como dependency.name, breaker.state e fallback.used. O ponto não é criar dashboard bonito. É responder rápido se o problema está no seu serviço, no fornecedor, na rede ou no excesso de retry.
Testando o comportamento
Teste de circuit breaker não deve depender de dormir trinta segundos em tempo real. Use configurações curtas no teste e um servidor httptest controlado. O objetivo é provar três coisas: abre depois de falhas suficientes, falha rápido quando está aberto e volta a permitir chamadas depois da janela.
func TestBreakerAbreDepoisDeFalhas(t *testing.T) {
calls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
http.Error(w, "down", http.StatusInternalServerError)
}))
defer srv.Close()
c := New(srv.URL)
for i := 0; i < 12; i++ {
_, _ = c.Analyze(context.Background(), Request{OrderID: "x", Amount: 100})
}
before := calls
_, err := c.Analyze(context.Background(), Request{OrderID: "x", Amount: 100})
if err == nil {
t.Fatal("esperava erro com breaker aberto")
}
if calls != before {
t.Fatal("breaker aberto não deveria chamar o servidor")
}
}
Em código real, ajuste o construtor para aceitar gobreaker.Settings no teste. Isso evita acoplar teste ao tempo de produção e permite simular janelas curtas. Também vale testar quais erros contam como falha. Um 400 Bad Request por payload inválido não deve ter o mesmo peso de 503 Service Unavailable.
Erros comuns em produção
O primeiro erro é combinar retry agressivo com breaker frouxo. Se cada requisição tenta três vezes e o breaker só abre depois de dezenas de falhas, você multiplica a carga antes de proteger o sistema. Retry precisa de backoff, jitter e limite. Em muitos fluxos de request síncrono, zero ou um retry já é suficiente.
O segundo é compartilhar um breaker único para dependências diferentes. API de pagamento, CEP, CRM e e-mail têm perfis de falha distintos. Um fornecedor ruim não deve abrir circuito de outro. Use um breaker por dependência e, em alguns casos, por operação crítica.
O terceiro é esquecer que cache também falha. Se Redis é otimização, falha de cache pode virar miss e seguir para banco. Se Redis é parte do contrato, como sessão ou lock, a degradação precisa ser mais cuidadosa. O tutorial de Go e Redis para cache ajuda a separar cache opcional de dependência crítica.
O quarto é esconder erro demais. Se o fallback sempre retorna uma resposta aparentemente normal, produto, suporte e SRE perdem o sinal. Prefira uma resposta parcial explícita, um 202 Accepted, uma mensagem de indisponibilidade temporária ou um status que permita ação do usuário.
O quinto é não conectar breaker a deploy. Durante incidentes, você pode precisar reduzir tráfego, desligar integração, trocar fornecedor ou ativar feature flag. Combine o padrão com feature flags em Go para ter controle operacional sem publicar código às pressas.
Checklist para APIs resilientes em Go
Antes de colocar um client protegido em produção, confira:
- toda chamada externa tem
context.Contexte timeout explícito; - status 5xx e 429 contam como falha de dependência;
- erros 4xx são tratados separadamente quando indicam bug de payload;
- breaker é separado por fornecedor/operação importante;
- fallback não viola segurança, pagamento ou privacidade;
- métricas mostram estado, falhas rápidas e uso de fallback;
- logs incluem dependência, operação, status e correlação de request;
- testes cobrem abertura, falha rápida e recuperação;
- retry, backoff e breaker não se multiplicam sem limite;
- runbook explica o que fazer quando o circuito abre.
Circuit breaker não deixa uma arquitetura automaticamente resiliente. Ele só torna a falha explícita, limitada e mensurável. A maturidade vem de combinar timeout, limitação de concorrência, idempotência, fila, observabilidade e mensagens honestas para o usuário.
Se você está se preparando para vagas Go backend, esse é um ótimo tema para entrevista porque conecta código, infraestrutura e operação. Muitas vagas Go no Brasil pedem APIs, microserviços, cloud, observabilidade e sistemas distribuídos. Para comparar como resiliência aparece em outras stacks brasileiras, acompanhe também o portal eu.dev.br reúne vagas de tecnologia no Brasil, onde padrões como timeout, retry, fila e observabilidade aparecem independentemente da linguagem.