← Voltar para o blog

API Go em Produção: Checklist de Segurança e Operação

Checklist para colocar uma API Go em produção com timeouts, graceful shutdown, logs, métricas, segurança, banco, testes, Docker e deploy seguro.

Resposta rápida: antes de colocar uma API Go em produção, confirme que ela tem timeouts, encerramento gracioso, validação e limites de entrada, erros sem vazamento de detalhes, logs estruturados, métricas, health checks, autenticação quando necessária, pool de banco dimensionado, testes e rollback. O binário compilar e o endpoint responder 200 OK são apenas o começo. Produção exige que o serviço continue previsível quando clientes ficam lentos, dependências falham, o tráfego cresce e uma nova versão precisa ser revertida.

Este guia organiza as verificações em uma ordem prática: aplicação, HTTP, segurança, banco, observabilidade, container, deploy e operação. Ele serve tanto para uma API feita com net/http quanto para projetos com Chi, Gin, Echo ou Fiber.

Checklist resumido para uma API Go em produção

ÁreaVerificação mínima
Configuraçãovariáveis externas, segredos fora do Git e validação no startup
HTTPtimeouts, limite de body, status corretos e headers seguros
Ciclo de vidagraceful shutdown e readiness durante o encerramento
Segurançaautenticação, autorização, CORS restrito e dependências verificadas
Bancopool limitado, contexto, transações curtas e migrations controladas
Resiliênciadeadlines, retries seletivos, idempotência e backpressure
Observabilidadelogs estruturados, métricas, tracing e request ID
Plataformahealth checks, usuário não root e imagem mínima
Entregatestes, deploy gradual, migrations compatíveis e rollback
Operaçãoalertas acionáveis, runbook e limites de capacidade conhecidos

Não transforme a lista em burocracia. Para uma API interna pequena, algumas implementações serão simples. O importante é que cada risco tenha uma resposta consciente, em vez de depender do comportamento padrão por acidente.

1. Valide a configuração ao iniciar

Configuração obrigatória deve falhar cedo. Se DATABASE_URL, chave de assinatura ou endereço de um serviço estiver ausente, é melhor o processo não ficar pronto do que descobrir o problema no primeiro request real.

package config

import (
	"fmt"
	"os"
	"time"
)

type Config struct {
	Addr         string
	DatabaseURL  string
	ShutdownWait time.Duration
}

func Load() (Config, error) {
	cfg := Config{
		Addr:         envOr("HTTP_ADDR", ":8080"),
		DatabaseURL:  os.Getenv("DATABASE_URL"),
		ShutdownWait: 15 * time.Second,
	}

	if cfg.DatabaseURL == "" {
		return Config{}, fmt.Errorf("DATABASE_URL é obrigatória")
	}
	return cfg, nil
}

func envOr(name, fallback string) string {
	if value := os.Getenv(name); value != "" {
		return value
	}
	return fallback
}

Não registre valores de segredos no log. Documente nomes e formatos em um .env.example, mas use o gerenciador de secrets da plataforma em produção. Para projetos com muitas fontes de configuração, veja o guia de Viper e variáveis de ambiente.

Também registre no startup apenas informações operacionais seguras, como versão, commit, ambiente e porta. Isso ajuda a identificar qual artefato está rodando sem expor credenciais.

2. Configure o servidor HTTP explicitamente

Evite publicar uma API usando apenas http.ListenAndServe(":8080", handler). Crie um http.Server para controlar limites e ciclo de vida:

server := &http.Server{
	Addr:              cfg.Addr,
	Handler:           handler,
	ReadHeaderTimeout: 5 * time.Second,
	IdleTimeout:       60 * time.Second,
	MaxHeaderBytes:    1 << 20,
}

ReadHeaderTimeout limita quanto tempo o servidor espera pelos headers e reduz exposição a clientes deliberadamente lentos. IdleTimeout controla conexões keep-alive ociosas. MaxHeaderBytes impõe um teto aos headers.

ReadTimeout e WriteTimeout exigem análise do tráfego. Um timeout rígido pode interromper uploads, downloads grandes, Server-Sent Events ou streaming. Defina-os conforme os endpoints e teste o comportamento atrás do proxy ou load balancer real. Timeouts da plataforma e do servidor devem ser coerentes.

Para o corpo, limite antes de decodificar:

func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()

	if err := dec.Decode(dst); err != nil {
		return fmt.Errorf("JSON inválido: %w", err)
	}
	return nil
}

O tamanho correto depende do contrato. Um endpoint de avatar não pode herdar o mesmo limite de um formulário JSON pequeno. Para uploads, trate multipart, armazenamento e verificação de tipo separadamente; consulte upload de arquivos em Go.

3. Faça graceful shutdown de verdade

Durante um deploy, a plataforma envia um sinal e remove a instância. Se o processo encerrar imediatamente, requests em andamento podem falhar e transações podem ser interrompidas.

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() {
	slog.Info("servidor iniciado", "addr", server.Addr)
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		slog.Error("servidor HTTP falhou", "error", err)
		os.Exit(1)
	}
}()

<-ctx.Done()
slog.Info("encerramento iniciado")

shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if err := server.Shutdown(shutdownCtx); err != nil {
	slog.Error("encerramento HTTP excedeu o prazo", "error", err)
}

Antes de chamar Shutdown, faça o readiness responder como indisponível e dê tempo para o balanceador parar de enviar conexões. Depois do HTTP, encerre consumidores de fila, schedulers e buffers, respeitando uma ordem definida.

Não use um contexto já cancelado para o shutdown; crie outro com prazo próprio. O guia de graceful shutdown em Go cobre a sequência completa.

4. Propague context e deadlines

O contexto do request deve chegar às operações que pertencem àquele request: banco, chamadas HTTP, filas e tarefas curtas. Assim, quando o cliente desconecta ou o prazo termina, trabalho desnecessário pode parar.

func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
	defer cancel()

	order, err := h.orders.FindByID(ctx, r.PathValue("id"))
	if err != nil {
		h.writeError(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, order)
}

Não escolha 2s por estética. Meça latência normal e de cauda, considere o orçamento total do request e deixe margem para as camadas acima. Um handler com prazo de dois segundos não pode fazer três chamadas sequenciais com timeout de dois segundos cada e esperar terminar dentro do orçamento.

Clientes HTTP também precisam de limite:

client := &http.Client{
	Timeout: 3 * time.Second,
}

Para controle fino, configure o Transport e use contexto por request. Nunca crie um http.Client novo para cada chamada; clientes e transports reutilizam conexões. Leia context, timeout e cancelamento e debug de clientes com httptrace.

5. Padronize validação, respostas e erros

Valide formato e regra de negócio em camadas claras. Diferencie pelo menos:

  • request malformado: 400 Bad Request;
  • autenticação ausente ou inválida: 401 Unauthorized;
  • usuário autenticado sem permissão: 403 Forbidden;
  • recurso inexistente: 404 Not Found;
  • conflito de estado ou unicidade: 409 Conflict;
  • limite excedido: 429 Too Many Requests;
  • erro inesperado: 500 Internal Server Error.

A resposta pública não deve conter stack trace, SQL, endereço interno ou token. Gere um identificador e registre o detalhe no servidor:

{
  "error": {
    "code": "internal_error",
    "message": "Não foi possível concluir a operação.",
    "request_id": "01J..."
  }
}

Erros de domínio estáveis são melhores que comparar strings. Use errors.Is e errors.As, envolva a causa com %w e traduza para HTTP apenas na borda da aplicação.

Documente o contrato com OpenAPI quando a API tiver consumidores independentes. O guia de oapi-codegen em Go mostra como reduzir divergência entre especificação, cliente e servidor.

6. Revise autenticação, autorização, CORS e CSRF

Autenticar responde “quem é?”. Autorizar responde “essa identidade pode fazer isto neste recurso?”. Não pare na validação do JWT: confira emissor, audiência, expiração, algoritmo esperado e permissões aplicáveis à operação.

Centralize a extração da identidade em middleware, mas mantenha verificações de ownership e autorização próximas ao caso de uso. Um usuário autenticado não deve conseguir acessar um pedido de outro usuário apenas trocando o ID na URL.

CORS não é um mecanismo de autenticação. Em APIs usadas por navegador, permita origens, métodos e headers necessários; evite refletir qualquer Origin, principalmente quando credenciais estão habilitadas. Consulte CORS em Go.

Se a autenticação usa cookies, avalie CSRF, SameSite, Secure e HttpOnly. Tokens em header mudam o modelo de risco, mas continuam exigindo proteção contra XSS e armazenamento inseguro. Veja autenticação e autorização para APIs Go e CSRF em Go.

7. Proteja a API contra abuso e saturação

Rate limiting deve proteger um recurso real: tentativas de login, criação de contas, consultas caras ou uso por cliente. A chave pode ser usuário, API key, tenant ou IP, conforme o produto. Em ambientes distribuídos, um limitador apenas em memória aplica o teto por instância, não necessariamente no sistema inteiro.

Além de requests por segundo, imponha limites de:

  • tamanho de payload;
  • quantidade de itens por página ou batch;
  • concorrência de operações caras;
  • número de jobs pendentes;
  • conexões simultâneas;
  • cardinalidade de filtros e exportações.

Retorne 429 com uma resposta consistente quando o cliente puder tentar depois. Para filas internas, use capacidade limitada e backpressure; não crie uma goroutine ilimitada por item. Leia rate limiting em APIs Go e worker pools com backpressure.

8. Dimensione banco e transações

O pool não deve crescer sem relação com a capacidade do banco. Em database/sql, configure limites e observe as estatísticas:

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)

Os números são ponto de partida, não recomendação universal. Considere quantidade de réplicas, limite total do PostgreSQL ou MySQL, concorrência e duração das queries. Se dez pods abrirem 25 conexões, o banco poderá receber 250 conexões.

Toda query deve receber contexto. Feche Rows, verifique rows.Err() e mantenha transações curtas. Não faça chamadas HTTP externas no meio de uma transação sem entender o impacto nos locks e na retenção da conexão.

Migrations precisam de uma estratégia compatível com deploy. Prefira mudanças expansivas em etapas: adicionar coluna ou tabela, publicar código compatível, migrar dados e só depois remover o formato antigo. Evite uma release que exige simultaneamente schema novo e binário novo sem possibilidade de rollback.

Veja pool de conexões com database/sql, migrations em Go e transações, locks e retry no PostgreSQL.

9. Use retries apenas quando forem seguros

Retry indiscriminado multiplica carga durante uma falha. Repita apenas erros transitórios, com quantidade limitada, backoff e jitter. Respeite o deadline original.

Operações de leitura costumam ser candidatas melhores. Escritas precisam de idempotência ou de um mecanismo que impeça efeitos duplicados. Se o cliente repetir POST /payments depois de um timeout, a API deve saber se o pagamento anterior foi criado.

Uma chave de idempotência associada ao cliente e ao payload pode armazenar o resultado da primeira execução. A regra precisa cobrir concorrência: duas chamadas simultâneas com a mesma chave não podem produzir dois efeitos.

Circuit breaker pode ajudar quando uma dependência falha de forma persistente, mas não substitui timeout. Primeiro limite a espera; depois avalie retries, breaker, cache ou degradação. Consulte idempotência, retry e DLQ e circuit breaker em Go.

10. Implemente logs estruturados e request ID

Use log/slog ou outra biblioteca estruturada para que logs possam ser filtrados por campos:

logger.InfoContext(r.Context(), "request concluído",
	"method", r.Method,
	"route", routePattern,
	"status", status,
	"duration_ms", duration.Milliseconds(),
	"request_id", requestID,
)

Prefira o padrão da rota, como /orders/{id}, em vez da URL concreta como label ou campo principal. Não registre senha, token, cookie, número completo de cartão nem body por padrão. Dados pessoais exigem política de retenção e acesso.

Campos úteis incluem serviço, ambiente, versão, request ID, trace ID, método, rota, status, duração e classe de erro. Evite duplicar a mesma exceção em todas as camadas; registre onde existe contexto suficiente para agir.

O artigo de slog para logging estruturado traz configuração, handlers e redaction.

11. Exponha métricas, traces e perfis com cuidado

Métricas básicas de HTTP seguem o modelo RED:

  • Rate: quantidade de requests;
  • Errors: erros por rota e status;
  • Duration: distribuição de latência.

Também acompanhe saturação: conexões de banco em uso e espera, fila de jobs, goroutines, CPU e memória. Labels devem ter cardinalidade limitada. Nunca use user_id, URL completa, mensagem de erro ou request ID como label de Prometheus.

Tracing distribuído ajuda quando um request atravessa banco, fila e outros serviços. Propague contexto e trace headers, mas aplique sampling coerente para controlar custo. Veja OpenTelemetry em Go.

pprof é excelente para investigar CPU, heap, bloqueios e goroutines, porém pode revelar detalhes sensíveis e consumir recursos. Não exponha /debug/pprof/ diretamente à internet. Restrinja por rede, autenticação ou acesso administrativo e consulte pprof em produção.

12. Separe liveness, readiness e startup

Um endpoint único /health costuma misturar perguntas diferentes:

  • liveness: o processo está vivo ou precisa ser reiniciado?
  • readiness: esta instância pode receber tráfego agora?
  • startup: a inicialização terminou dentro do tempo esperado?

Liveness deve ser simples. Se ela falhar porque um serviço externo está temporariamente indisponível, todas as réplicas podem reiniciar ao mesmo tempo e piorar a instabilidade.

Readiness pode ficar falsa durante startup, shutdown ou quando a instância não consegue atender de forma útil. Mesmo assim, pense antes de atrelar o resultado a cada dependência: retirar todas as instâncias porque um serviço secundário falhou pode causar indisponibilidade total.

Não devolva detalhes internos, credenciais ou configuração nos health checks. O guia de liveness, readiness e startup em Go oferece exemplos para containers e Kubernetes.

13. Produza uma imagem de container pequena e previsível

Use multi-stage build e copie apenas o necessário para a imagem final:

FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]

Fixe versões de imagens de forma compatível com sua política de atualização e verifique vulnerabilidades. Se a aplicação usa CGO, certificados adicionais, timezone files ou executa comandos externos, uma imagem static pode não atender; escolha conscientemente.

O binário deve escrever logs em stdout/stderr, não depender de filesystem gravável e receber configuração externamente. Não copie .git, .env ou ferramentas de desenvolvimento para o estágio final.

Leia Docker com Go e o comparativo de onde hospedar uma API Go.

14. Automatize verificações antes do deploy

Uma pipeline mínima pode executar:

gofmt -w .
go vet ./...
go test ./...
go test -race ./...
go build ./cmd/api
govulncheck ./...

Na CI, normalmente você verifica se gofmt produziria mudanças em vez de alterar os arquivos. Adicione lint e testes de integração conforme o projeto. Race detector aumenta custo e não prova ausência de races, mas encontra problemas reais quando os caminhos são exercitados.

Testes relevantes para API incluem:

  • handlers com entradas válidas e inválidas;
  • autenticação e autorização por recurso;
  • timeout e cancelamento;
  • constraints e rollback no banco;
  • idempotência sob chamadas repetidas e concorrentes;
  • compatibilidade do contrato OpenAPI;
  • shutdown com request em andamento.

Para dependências reais, Testcontainers em Go reduz a distância entre mock e comportamento do banco. Para a cadeia de dependências, use govulncheck.

15. Planeje deploy e rollback antes de precisar deles

Um deploy seguro não começa depois do incidente. Defina:

  1. como a versão é identificada;
  2. quais sinais determinam sucesso;
  3. quanto tempo a observação dura;
  4. quem ou o que inicia rollback;
  5. se o schema continua compatível com o binário anterior.

Rolling deploy, canário e blue-green reduzem risco de formas diferentes. Independentemente da estratégia, monitore erro, latência, saturação e métricas de negócio. Uma release pode ter 200 OK e ainda criar pedidos duplicados ou deixar de publicar eventos.

Não dependa de “rebuild da mesma tag”. Artefatos devem ser imutáveis e rastreáveis ao commit. Se feature flags controlam uma mudança arriscada, defina owner, valor padrão e data para remover a flag. Veja feature flags em Go e GoReleaser, checksums e SBOM.

16. Prepare alertas e runbooks acionáveis

Alertar cada erro individual produz ruído. Prefira sinais ligados ao impacto:

  • taxa de erro acima do normal;
  • latência de cauda fora do objetivo;
  • fila crescendo sem recuperação;
  • pool de banco esperando conexão;
  • ausência de consumo ou publicação;
  • readiness insuficiente para atender o tráfego;
  • esgotamento de CPU ou memória.

O alerta deve apontar para um runbook curto: dashboards, logs, mudanças recentes, como reduzir impacto e como reverter. Registre limites conhecidos, como capacidade aproximada por réplica e gargalo dominante.

Faça exercícios: interrompa uma dependência em staging, envie SIGTERM, force timeout e confirme que métricas e logs permitem explicar o ocorrido. Resiliência não é apenas ter código de fallback; é conseguir detectar, decidir e recuperar.

Checklist final antes de publicar

Aplicação e HTTP

  • Configuração obrigatória é validada no startup.
  • Segredos ficam fora do repositório e dos logs.
  • Servidor HTTP possui ReadHeaderTimeout, IdleTimeout e limites adequados.
  • Bodies, uploads, páginas e batches têm tamanho máximo.
  • Clientes HTTP reutilizam conexões e possuem timeout.
  • Contexto e cancelamento chegam ao banco e às dependências.
  • Graceful shutdown foi testado com requests em andamento.
  • Erros públicos não vazam detalhes internos.

Segurança

  • Autenticação valida emissor, audiência, expiração e algoritmo esperado.
  • Autorização é verificada por ação e recurso.
  • CORS permite somente origens e métodos necessários.
  • Cookies usam atributos seguros e o risco de CSRF foi tratado.
  • Rate limits e limites de concorrência protegem operações caras.
  • govulncheck e atualização de dependências fazem parte do processo.

Dados e resiliência

  • Pool de conexões considera todas as réplicas.
  • Queries usam contexto; Rows e transações são encerrados corretamente.
  • Migrations preservam compatibilidade durante deploy e rollback.
  • Retries são limitados a falhas transitórias e operações seguras.
  • Escritas repetíveis usam idempotência quando necessário.
  • Filas e goroutines possuem capacidade e backpressure.

Observabilidade e plataforma

  • Logs são estruturados, correlacionáveis e não contêm segredos.
  • Métricas cobrem tráfego, erros, duração e saturação.
  • Labels possuem cardinalidade controlada.
  • Tracing propaga contexto entre serviços relevantes.
  • pprof e endpoints administrativos não estão públicos.
  • Liveness, readiness e startup respondem perguntas diferentes.
  • Container roda sem root e contém apenas artefatos necessários.

Entrega e operação

  • Formatação, vet, testes, race detector e build passam na CI.
  • Testes de integração cobrem banco e falhas importantes.
  • O artefato é imutável e identifica commit e versão.
  • Estratégia de rollout e critérios de rollback estão definidos.
  • Alertas apontam impacto e possuem runbook.
  • A equipe sabe onde olhar durante os primeiros minutos do deploy.

Perguntas frequentes

O que uma API Go precisa antes de entrar em produção?

Precisa, no mínimo, de configuração validada, timeouts, graceful shutdown, limites de entrada, tratamento seguro de erros, logs, métricas, health checks, pool de banco controlado, testes e rollback. Autenticação, filas, tracing e outras camadas dependem do produto, mas devem ser avaliadas explicitamente.

Quais timeouts devo configurar?

Comece por ReadHeaderTimeout e IdleTimeout no servidor. Avalie ReadTimeout e WriteTimeout conforme uploads, downloads e streaming. Em clientes HTTP, use timeout global ou contexto por chamada. O valor deve respeitar o orçamento do request e os timeouts do proxy e da plataforma.

Como fazer graceful shutdown?

Capture SIGINT e SIGTERM, retire a instância do readiness, pare de aceitar tráfego, chame http.Server.Shutdown com prazo e encerre workers e conexões na ordem correta. Teste a sequência; não presuma que o orquestrador dará tempo ilimitado.

Preciso usar Gin, Echo, Fiber ou Chi?

Não. net/http é suficiente para muitas APIs. Routers e frameworks podem melhorar ergonomia, grupos de rotas e middleware, mas as exigências de produção continuam iguais. Compare as opções no guia de frameworks HTTP para Go.

Health check deve testar o banco?

Não na liveness. Readiness pode considerar dependências essenciais, mas uma verificação mal desenhada pode remover todas as réplicas durante uma falha externa. Decida com base em qual resposta a instância ainda consegue oferecer e se reiniciar o processo ajudaria.

Conclusão

Uma API Go pronta para produção não é a que tem mais middleware; é a que possui limites claros, falhas previsíveis e sinais suficientes para operação. Configure o HTTP, propague deadlines, encerre com cuidado, proteja dados, dimensione o pool e automatize testes. Depois, prove que o deploy pode ser observado e revertido.

Use este checklist em pull requests de novos serviços e antes de mudanças importantes. Nem todos os itens precisam de uma plataforma sofisticada: http.Server, context, slog, testes e health checks simples já resolvem uma parte grande do problema quando aplicados com intenção.

Se você está começando, siga primeiro o tutorial de API REST em Go. Para aprofundar a operação, continue com graceful shutdown, OpenTelemetry, Docker e deploy de APIs Go.

Fontes