Microservices com Go: Arquitetura e Exemplo Prático
O que são microservices? Microservices (microsserviços) são uma arquitetura que separa capacidades de negócio em serviços com deploy independente, que se comunicam por APIs ou mensagens. Microservices com Go usam a linguagem para implementar esses componentes com HTTP, gRPC ou consumidores de filas. Go oferece concorrência e uma biblioteca HTTP padrão, mas não elimina falhas de rede nem decide os limites do domínio. Comece com um monólito modular e extraia serviços quando houver uma necessidade concreta de deploy independente, escala diferente ou autonomia entre equipes.
Um desenho inicial saudável costuma ter poucos serviços, cada um dono dos próprios dados, comunicação síncrona com timeout e operações assíncronas idempotentes. Antes de aumentar a quantidade de componentes, garanta logs estruturados, métricas, tracing, health checks e graceful shutdown.
| Decisão | Escolha inicial | Mude quando… |
|---|---|---|
| Arquitetura | Monólito modular | Um domínio precisa escalar ou ser implantado separadamente |
| API externa | REST/JSON | Clientes controlados precisam de contrato tipado e performance |
| Comunicação interna | HTTP simples | gRPC/ConnectRPC reduz duplicação entre vários clientes |
| Trabalho em segundo plano | Fila/evento | A resposta não precisa esperar o processamento |
| Banco | PostgreSQL por domínio lógico | Volume, latência ou disponibilidade exigem outra tecnologia |
| Deploy | Docker + serviço gerenciado | Kubernetes se paga pela escala e padronização operacional |
Este guia mostra como tomar essas decisões, implementar um serviço HTTP em Go e preparar a arquitetura para falhas reais de rede — não apenas desenhar caixas em um diagrama.
Microservices ou microsserviços?
No mercado brasileiro, microservices e microsserviços são usados como sinônimos. A ideia é dividir um sistema em serviços com responsabilidade clara e implantação independente, como descreve o padrão Microservice Architecture. Essa independência tem um custo: chamadas que antes eram locais passam a depender da rede, e mudanças entre domínios precisam de contratos e coordenação.
“Pequeno”, porém, não significa uma tabela, um endpoint ou cinquenta linhas de código. Um serviço deve representar uma capacidade de negócio, por exemplo:
- catálogo;
- pedidos;
- pagamentos;
- identidade;
- entrega;
- notificações.
Dividir usuarios, enderecos e telefones em três serviços porque existem três tabelas cria tráfego de rede sem criar autonomia. Um limite melhor seria “identidade e perfil”, com regras e dados mantidos pelo mesmo componente.
Quando microservices fazem sentido
Considere extrair um serviço quando pelo menos uma destas pressões já existe:
- Deploy independente: uma área muda frequentemente e não deveria exigir a implantação do sistema inteiro.
- Escala diferente: processamento de imagens ou cálculo de frete consome recursos muito diferentes do painel administrativo.
- Isolamento de falhas: uma integração instável não pode derrubar o checkout.
- Limite de domínio estável: o time consegue explicar o que pertence e o que não pertence ao serviço.
- Autonomia de equipe: grupos diferentes precisam decidir e entregar sem coordenar cada commit.
- Requisito tecnológico específico: uma carga realmente precisa de armazenamento, runtime ou política operacional distinta.
Microservices provavelmente são prematuros quando uma única equipe mantém tudo, o domínio muda toda semana, não há observabilidade centralizada ou o deploy atual ainda é manual. Nesse cenário, uma chamada de função dentro de um monólito é mais rápida, confiável e fácil de depurar que uma chamada pela rede.
A alternativa não é um “monólito bagunçado”. Um monólito modular com Clean Architecture pode separar domínios em pacotes, contratos e testes. Se um limite se provar útil, a extração posterior fica muito mais segura.
Arquitetura de referência
Imagine um e-commerce com três capacidades principais:
cliente
|
v
API Gateway
|
+------> catálogo ------> banco do catálogo
|
+------> pedidos ------> banco de pedidos
|
+---- HTTP/gRPC ----> pagamentos
|
+---- evento -------> notificações
O gateway autentica, limita tráfego e encaminha requisições; ele não deve concentrar todas as regras do produto. O serviço de pedidos é dono do ciclo de vida do pedido. Pagamentos decide como autorizar uma cobrança. Notificações reage a eventos sem bloquear a resposta principal.
Regras importantes do desenho
- Um serviço não lê diretamente as tabelas de outro.
- Contratos de API são versionados e compatíveis durante o rollout.
- Toda chamada de rede tem timeout e contexto cancelável.
- Eventos podem ser entregues mais de uma vez; consumidores devem ser idempotentes.
- Falhas parciais são esperadas e aparecem em métricas e traces.
- Dados sensíveis não são copiados para logs ou eventos sem necessidade.
Estrutura de um serviço Go
Não existe uma árvore obrigatória, mas esta mantém o executável, o domínio e os adaptadores separados sem transformar cada função em uma camada:
order-service/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── order/
│ │ ├── service.go
│ │ ├── repository.go
│ │ └── model.go
│ ├── postgres/
│ │ └── order_repository.go
│ └── httpapi/
│ └── handler.go
├── migrations/
├── Dockerfile
├── go.mod
└── go.sum
Use internal/ para impedir imports externos acidentais. Crie interfaces no pacote que consome a dependência, não em um pacote genérico cheio de abstrações futuras. Para um serviço pequeno, cmd/api, internal/order e internal/httpapi já podem ser suficientes.
Exemplo: serviço HTTP com health checks e shutdown
O exemplo abaixo usa apenas a biblioteca padrão e mostra timeouts, endpoints de saúde e encerramento controlado. Requer Go 1.22 ou superior: os padrões de rota com método (GET /pedidos/{id}) e Request.PathValue foram adicionados no Go 1.22. Confira sua instalação com go version.
Este é um serviço didático isolado, não um sistema distribuído completo: o pedido é montado em memória, sem persistência, autenticação ou chamada a outro serviço. /readyz retorna sucesso fixo; em produção, deve representar a capacidade real de atender tráfego.
package main
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Pedido struct {
ID string `json:"id"`
Status string `json:"status"`
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("GET /livez", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
// Em produção, verifique apenas dependências essenciais e rapidamente.
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
})
mux.HandleFunc("GET /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
pedido := Pedido{ID: r.PathValue("id"), Status: "recebido"}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(pedido)
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
logger.Info("servidor iniciado", "addr", server.Addr)
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("falha no servidor", "erro", err)
os.Exit(1)
}
}()
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
logger.Error("falha no shutdown", "erro", err)
}
}
Como executar e testar o serviço
Crie o projeto no terminal:
mkdir pedidos-service
cd pedidos-service
go mod init example.com/pedidos-service
Salve o código acima em main.go e inicie:
go run .
Em outro terminal, teste os endpoints:
curl -i http://localhost:8080/livez
curl -i http://localhost:8080/readyz
curl -i http://localhost:8080/pedidos/pedido-123
Os três retornam HTTP 200. O último retorna o JSON {"id":"pedido-123","status":"recebido"}. Pressione Ctrl+C no terminal do servidor para acionar o encerramento controlado. Os campos de timeout e Server.Shutdown estão documentados na referência oficial de net/http.Server: Shutdown aguarda conexões ativas até o prazo do contexto; se esse prazo expirar, retorna um erro.
Para dar o próximo passo, substitua o pedido fixo por um repositório usando o guia de Go com PostgreSQL. Só extraia outro serviço quando esse domínio precisar evoluir separadamente; criar dois executáveis que sempre precisam de deploy conjunto não demonstra a autonomia pretendida.
/livez responde se o processo está vivo. /readyz informa se ele pode receber tráfego. Não coloque uma consulta pesada em cada probe: o health check pode virar a causa da sobrecarga. Veja a implementação completa em health checks com liveness e readiness e o fluxo de sinais em graceful shutdown em Go.
Comunicação síncrona: REST, gRPC ou ConnectRPC
REST/JSON
REST funciona bem para APIs públicas, integrações heterogêneas e operações orientadas a recursos. O ecossistema HTTP é conhecido, curl ajuda no diagnóstico e proxies entendem o tráfego. Para construir handlers completos, siga o guia de API REST com Go.
Uma chamada interna precisa de cliente reutilizável, contexto e timeout:
func buscarCliente(ctx context.Context, httpClient *http.Client, id string) error {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"http://clientes:8080/clientes/"+id,
nil,
)
if err != nil {
return err
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("serviço de clientes retornou %s", resp.Status)
}
return nil
}
O chamador deve criar o contexto com um prazo coerente com o orçamento total da requisição. Se o endpoint externo tem dois segundos para responder, não configure três dependências sequenciais com dois segundos cada. O guia de context, timeout e cancelamento explica como propagar esse orçamento.
gRPC
gRPC com Go e Protobuf é útil quando vários serviços compartilham contratos tipados, existem clientes em diferentes linguagens ou streaming faz parte do caso de uso. A geração de código reduz divergência entre cliente e servidor, mas adiciona toolchain, políticas de compatibilidade e requisitos de infraestrutura HTTP/2.
ConnectRPC
ConnectRPC em Go também usa Protobuf e clientes gerados, porém integra handlers diretamente ao net/http e facilita chamadas unary por HTTP/JSON. É uma opção pragmática para equipes que querem contratos tipados e interoperabilidade sem abandonar ferramentas HTTP familiares.
A escolha não precisa ser única: REST pode atender parceiros, gRPC ou ConnectRPC pode ligar serviços internos, e eventos podem cuidar de processos assíncronos.
Comunicação assíncrona: eventos e filas
Use mensageria quando o produtor não precisa esperar o consumidor terminar. Exemplos:
- enviar email após a criação do pedido;
- gerar nota fiscal;
- atualizar um índice de busca;
- processar imagem ou relatório;
- integrar sistemas com disponibilidade diferente.
Kafka, RabbitMQ, NATS e SQS resolvem problemas relacionados, mas têm modelos operacionais diferentes. O guia de mensageria em Go ajuda a escolher.
Entrega duplicada é normal
Se um consumidor processa a mensagem e cai antes de confirmar, o broker pode entregá-la novamente. Por isso, “cobrar pedido” não pode significar simplesmente executar a cobrança a cada recebimento. Use uma chave idempotente e registre o resultado:
chave: cobrar:pedido-123
primeira entrega -> executa e grava resultado
segunda entrega -> devolve o resultado já gravado
Aprofunde o mecanismo em idempotência, retry e dead-letter queue.
Banco e evento precisam concordar
Este fluxo é perigoso:
- grava pedido no PostgreSQL;
- publica
PedidoCriadono broker; - o processo cai entre as duas operações.
O pedido existe, mas o evento nunca foi publicado. O Outbox Pattern em Go resolve isso gravando a alteração e o evento pendente na mesma transação local; um worker publica a outbox posteriormente.
Resiliência: timeout antes de retry
Uma dependência lenta pode consumir todas as goroutines, conexões e workers disponíveis. A ordem prática é:
- definir timeout;
- limitar concorrência;
- classificar erros transitórios;
- aplicar retry com backoff e jitter apenas quando a operação for segura;
- usar circuit breaker se a dependência continuar falhando;
- medir o comportamento.
Retry indiscriminado aumenta a carga justamente durante um incidente. Nunca repita automaticamente uma operação não idempotente sem chave ou protocolo de deduplicação. Veja exemplos em circuit breaker e retries com Go.
Evite também cadeias longas como gateway → pedidos → clientes → endereços → antifraude. A disponibilidade da requisição passa a depender de todos os componentes, e a latência se acumula. Prefira dados locais derivados por eventos quando consistência eventual for aceitável, ou redesenhe a API para reduzir viagens.
Dados e consistência
“Banco por serviço” significa propriedade, não necessariamente uma máquina exclusiva. O serviço de pedidos pode ter seu schema PostgreSQL, enquanto pagamentos tem outro schema ou database no mesmo cluster. O contrato é que pagamentos não consulta as tabelas de pedidos diretamente.
Para operações que atravessam domínios, escolha conscientemente entre:
- consistência forte local: transação dentro do banco de um serviço;
- consistência eventual: eventos atualizam outros domínios depois;
- orquestração: um processo coordena etapas e compensações;
- coreografia: consumidores reagem a eventos sem um coordenador central.
Não tente reproduzir uma transação SQL distribuída por meio de várias chamadas HTTP e rollbacks improvisados. Modele estados intermediários como pagamento_pendente, confirmado e falhou, e permita retomada segura.
Observabilidade obrigatória
Em um monólito, uma stack trace pode revelar quase todo o caminho. Em microservices, uma ação cruza processos e máquinas. Você precisa de três sinais complementares:
- logs estruturados: evento, nível, serviço, versão e request ID;
- métricas: taxa de requisições, erros, duração, saturação e filas;
- traces distribuídos: caminho e tempo gasto entre serviços.
Use OpenTelemetry para propagar contexto e exportar traces e métricas. O guia de OpenTelemetry em Go mostra a instrumentação. Para logs, prefira campos estáveis com log/slog; não transforme user_id, URL completa ou mensagem de erro em labels de métrica, pois isso cria cardinalidade alta.
Antes do deploy, defina indicadores de serviço, por exemplo:
- proporção de respostas sem erro;
- percentil 95 ou 99 de latência;
- idade da mensagem mais antiga na fila;
- taxa de retry e mensagens na DLQ;
- uso de CPU, memória, conexões e goroutines.
Docker e Kubernetes
Um serviço Go cabe bem em imagem mínima com multi-stage build:
ARG GO_VERSION=1.27
FROM golang:${GO_VERSION}-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]
O guia de Go com Docker cobre cache de camadas, scratch, distroless, certificados e Docker Compose.
No Kubernetes, configure ao menos:
readinessProbeapontando para/readyz;livenessProbeapontando para/livez;resources.requestseresources.limitsmedidos;terminationGracePeriodSecondscompatível com o shutdown;- política de rollout que preserve capacidade;
- autoscaling baseado em sinal relevante, não apenas CPU por hábito.
Kubernetes não corrige contratos frágeis nem falta de observabilidade. Ele reinicia processos e oferece primitives de operação; a aplicação ainda precisa lidar corretamente com timeout, sinais, duplicidade e indisponibilidade. Para manifests e deploy, veja Go no Kubernetes.
Segurança entre serviços
Uma rede interna não deve ser tratada como confiável por padrão. Defina:
- TLS em trânsito conforme o ambiente e risco;
- identidade do workload ou credencial de curta duração;
- autenticação em cada fronteira relevante;
- autorização baseada na ação e no recurso;
- rotação de segredos sem rebuild da imagem;
- limites de payload e rate limiting;
- validação de entrada mesmo com Protobuf;
- logs sem token, senha ou dado pessoal desnecessário.
O gateway pode validar a credencial externa, mas serviços críticos ainda precisam saber qual identidade realizou a ação e verificar permissão. Para APIs de usuário, combine com autenticação e autorização em Go.
Testes para uma arquitetura distribuída
Organize a pirâmide de testes por custo:
- unitários: regras de domínio sem rede;
- integração: repository com banco real e adapters externos;
- contrato: cliente e servidor concordam sobre schema, status e compatibilidade;
- componente: serviço iniciado com dependências controladas;
- ponta a ponta: poucos fluxos críticos no ambiente integrado;
- resiliência: timeout, duplicidade, indisponibilidade e retomada.
Testcontainers em Go permite iniciar PostgreSQL, Redis ou brokers reais durante testes de integração. Para APIs, teste também cancelamento de contexto, limite de corpo, erro da dependência e encerramento durante requisições em andamento.
Armadilhas comuns
Um serviço por entidade
Gera muitos componentes sem limites de negócio. Modele capacidades, não tabelas.
Biblioteca compartilhada com todo o domínio
Um pacote comum enorme acopla deploys e transforma mudanças locais em atualizações coordenadas. Compartilhe infraestrutura pequena e estável; contratos de domínio devem evoluir por APIs ou schemas versionados.
Retry em todas as camadas
Gateway, serviço e SDK repetem a mesma chamada e multiplicam tráfego. Defina qual camada é responsável e limite o orçamento total.
Eventos sem versionamento
Consumidores antigos continuam ativos durante rollouts. Prefira mudanças aditivas, campos opcionais e uma política explícita de compatibilidade.
Kubernetes cedo demais
Se dois serviços cabem em uma plataforma gerenciada simples, um cluster pode adicionar mais operação que valor. Escolha a ferramenta proporcional ao problema.
Observabilidade depois do incidente
Sem correlação, métricas e dashboards mínimos, o primeiro incidente distribuído vira tentativa e erro. Instrumente antes de extrair muitos serviços.
Checklist de produção
- O serviço representa uma capacidade de negócio clara
- Existe motivo concreto para deploy separado
- O serviço é dono do próprio schema e das migrações
- Toda chamada externa usa contexto e timeout
- Retries têm limite, backoff, jitter e operação idempotente
- Consumidores toleram mensagens duplicadas
- Publicação de eventos críticos usa outbox ou garantia equivalente
- Logs são estruturados e não vazam segredos
- Métricas cobrem tráfego, erros, duração e saturação
- Traces propagam contexto entre serviços
- Liveness e readiness têm semânticas diferentes
- O processo trata SIGTERM com graceful shutdown
- A imagem roda sem privilégios e tem versão identificável
- Contratos passam por teste de compatibilidade
- O rollback foi pensado junto com a migração de dados
- Alertas representam impacto para o usuário, não apenas CPU alta
Trilha prática de estudo
Se você quer demonstrar microservices em um portfólio, não crie quinze serviços vazios. Construa dois ou três componentes que mostrem decisões reais:
- uma API REST em Go para pedidos;
- PostgreSQL com transações e migrações;
- um consumidor de fila idempotente;
- outbox para publicação confiável;
- tracing entre API e worker;
- Docker Compose para desenvolvimento;
- testes de integração com containers;
- README explicando por que cada limite existe.
Esse projeto demonstra mais maturidade que um diagrama complexo sem tratamento de falhas. Em processos seletivos, prepare-se para explicar consistência, timeout, deploy, monitoramento e o que você manteria como monólito. Veja também as perguntas de entrevista Go e as vagas Go backend para observar quais competências aparecem no mercado brasileiro.
Conclusão
Microservices com Go funcionam bem quando a simplicidade da linguagem é acompanhada por disciplina operacional. O caminho seguro é começar modular, extrair por necessidade, manter poucos contratos claros e assumir que rede, broker e dependências vão falhar.
Para o primeiro serviço, use net/http, PostgreSQL, timeouts, logs com slog, health checks e shutdown controlado. Depois adicione mensageria somente ao fluxo que realmente pode ser assíncrono. Quando a arquitetura crescer, gRPC ou ConnectRPC, OpenTelemetry, outbox e Kubernetes passam a resolver problemas concretos — não a cumprir uma lista de tecnologias.
Referências técnicas
- Microservice Architecture — microservices.io: definição, deploy independente e custos da arquitetura distribuída.
- Routing Enhancements for Go 1.22 — Go Blog: métodos HTTP nos padrões de rota e parâmetros com
PathValue. net/http— documentação oficial: comportamento de servidor, cliente, timeouts e shutdown.- gRPC deadlines — documentação oficial: definição de prazos; gRPC não estabelece deadline por padrão.
- Liveness, readiness e startup probes — Kubernetes: diferenças entre reiniciar um container e retirar uma instância do tráfego.