Janeiro 2026 · ~15 min

Microservices com Go: Arquitetura e Exemplo Prático

Entenda o que são microservices e quando usar Go. Execute um serviço HTTP com health checks, timeout e shutdown e compare monólito, REST, gRPC e eventos.

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ãoEscolha inicialMude quando…
ArquiteturaMonólito modularUm domínio precisa escalar ou ser implantado separadamente
API externaREST/JSONClientes controlados precisam de contrato tipado e performance
Comunicação internaHTTP simplesgRPC/ConnectRPC reduz duplicação entre vários clientes
Trabalho em segundo planoFila/eventoA resposta não precisa esperar o processamento
BancoPostgreSQL por domínio lógicoVolume, latência ou disponibilidade exigem outra tecnologia
DeployDocker + serviço gerenciadoKubernetes 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:

  1. Deploy independente: uma área muda frequentemente e não deveria exigir a implantação do sistema inteiro.
  2. Escala diferente: processamento de imagens ou cálculo de frete consome recursos muito diferentes do painel administrativo.
  3. Isolamento de falhas: uma integração instável não pode derrubar o checkout.
  4. Limite de domínio estável: o time consegue explicar o que pertence e o que não pertence ao serviço.
  5. Autonomia de equipe: grupos diferentes precisam decidir e entregar sem coordenar cada commit.
  6. 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:

  1. grava pedido no PostgreSQL;
  2. publica PedidoCriado no broker;
  3. 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 é:

  1. definir timeout;
  2. limitar concorrência;
  3. classificar erros transitórios;
  4. aplicar retry com backoff e jitter apenas quando a operação for segura;
  5. usar circuit breaker se a dependência continuar falhando;
  6. 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:

  • readinessProbe apontando para /readyz;
  • livenessProbe apontando para /livez;
  • resources.requests e resources.limits medidos;
  • terminationGracePeriodSeconds compatí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:

  1. unitários: regras de domínio sem rede;
  2. integração: repository com banco real e adapters externos;
  3. contrato: cliente e servidor concordam sobre schema, status e compatibilidade;
  4. componente: serviço iniciado com dependências controladas;
  5. ponta a ponta: poucos fluxos críticos no ambiente integrado;
  6. 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:

  1. uma API REST em Go para pedidos;
  2. PostgreSQL com transações e migrações;
  3. um consumidor de fila idempotente;
  4. outbox para publicação confiável;
  5. tracing entre API e worker;
  6. Docker Compose para desenvolvimento;
  7. testes de integração com containers;
  8. 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

Próximos passos

Perguntas frequentes

O que são microservices e como usar Go nessa arquitetura?

Microservices, ou microsserviços, são uma arquitetura que separa capacidades de negócio em serviços com deploy independente e comunicação por APIs ou mensagens. Go implementa esses serviços com HTTP, gRPC e consumidores de filas; a linguagem não define os limites do domínio nem elimina falhas de rede. Comece com um monólito modular se não houver necessidade concreta de separar deploys ou escala.

Go é uma boa linguagem para microservices?

Sim. Go gera binários fáceis de distribuir, tem concorrência nativa, biblioteca HTTP madura, inicialização rápida e bom ecossistema para gRPC, mensageria, observabilidade e containers. Isso reduz o custo de empacotar e operar serviços, mas não elimina a complexidade de uma arquitetura distribuída.

Quando devo dividir um sistema Go em microservices?

Divida quando existirem limites de domínio claros e uma necessidade concreta de deploy, escala, disponibilidade ou autonomia de equipe. Se o produto ainda é pequeno, o domínio muda muito ou uma única equipe mantém tudo, um monólito modular costuma ser mais simples e barato.

REST, gRPC ou mensageria entre microservices Go?

Use REST para integrações HTTP simples e públicas, gRPC ou ConnectRPC para chamadas internas tipadas e mensageria para processos assíncronos e desacoplados. Em sistemas reais, é comum combinar os três, sempre com timeout, idempotência e observabilidade.

Como evitar chamadas em cascata entre microservices?

Mantenha APIs orientadas a capacidades, limite a profundidade das dependências, defina deadlines, use cache apenas quando houver estratégia de invalidação e mova trabalhos que não precisam de resposta imediata para eventos ou filas. Uma requisição que atravessa muitos serviços fica mais lenta e mais vulnerável a falhas.

Cada microservice precisa ter seu próprio banco?

O princípio é que cada serviço seja dono dos seus dados e do seu schema. Isso não exige uma instância física de banco por serviço: bancos ou clusters podem ser compartilhados operacionalmente, desde que tabelas e alterações não sejam acessadas diretamente por outros serviços.

Como fazer deploy de microservices Go?

Compile um binário reproduzível, empacote-o em uma imagem multi-stage mínima, rode como usuário sem privilégios, exponha liveness e readiness, configure limites de recursos e implemente graceful shutdown. Kubernetes é útil quando a escala e a operação justificam o custo; Docker Compose atende bem desenvolvimento local.