Como escrevo serviços HTTP em Go depois de 13 anos

Desenvolvedores de Go estão debatendo como estruturar serviços HTTP para que sejam testáveis e fáceis de manter, com foco em padrões como minimizar a lógica em `main`, passar dependências explicitamente para handlers e evitar structs de configuração ou de servidor gigantescas do tipo “god”. Muitos preferem funções pequenas e composáveis e injeção explícita de dependências (às vezes via bibliotecas como o fx da Uber), enquanto outros alertam contra overengineering e preferem mains mais simples e parecidos com scripts para serviços sob medida. Um tema recorrente é usar tipos e parsing (por exemplo, “parse, don’t validate”) para codificar invariantes e reduzir bugs, em vez de depender de validação ad hoc espalhada pelo código.

Estrutura de main e testabilidade

  • Muitos gostam de um main() minúsculo que delega para uma função run(), para que os erros possam ser retornados e a maior parte da lógica se torne testável e reutilizável.
  • Outros preferem um estilo de main.go em “script plano” para serviços internos, mantendo o fluxo de controle de alto nível visível em vez de empurrado para helpers.
  • Consenso: mantenha pequeno o código de bootstrap que não é testável, mas não às custas da legibilidade; o próprio main() não pode ser chamado a partir de testes, o que motiva a indirection.

Handlers, dependências e DI

  • Há forte apoio a dependências explícitas: handlers devem receber o que precisam por argumentos ou por structs por handler, e não ficar escondidos em uma enorme struct de servidor.
  • Alguns querem “clareza brutal” mesmo que isso signifique muitos parâmetros; outros agrupam dependências em structs de contexto/ambiente ou structs de handler para evitar assinaturas gigantes.
  • Há tensão entre simplicidade e acoplamento: enormes structs de servidor “god” e construtores com muitos argumentos cheiram a acoplamento excessivo.

Configuração e padrões de options

  • Passar uma config global mutável por aí é amplamente criticado; isso acopla subsistemas e pode causar bugs sutis de ordem.
  • Padrões preferidos: config imutável ou “congelada”; copiar a config para campos internos; structs de config por pacote; e “functional options” ou structs de options para construtores.
  • Preocupação: padrões de options espertos demais adicionam boilerplate e prejudicam a descoberta; structs de config simples muitas vezes vencem no longo prazo.

Validação vs “parse, don’t validate” e tipos

  • Muitos defendem encapsular primitivos (por exemplo, Username) em tipos dedicados construídos por parsing/validação para tornar estados inválidos irrepresentáveis.
  • Há debate sobre os zero values do Go: eles enfraquecem garantias quando structs podem ser instanciadas sem passar por construtores.
  • Alguns veem esse padrão como exagero ou algo incômodo em Go; outros o veem como essencial para evitar bugs “stringly typed”.

Estratégias de teste

  • Vários preferem testes no estilo end-to-end que sobem um servidor de teste com dependências falsas, exercitando HTTP e os caminhos de middleware.
  • Outros focam em testar a lógica dos handlers em unidade, injetando dependências e usando pequenas funções auxiliares.

Spec-first e geração de código

  • Alguns participantes gostam de fluxos OpenAPI-first com geradores (por exemplo, ogen, oapi-codegen), para evitar código repetitivo de validação e roteamento.
  • Outros acham tediosa a autoria de OpenAPI, mas observam que ferramentas e autoria assistida por LLM ajudam, e que IDLs (incluindo gRPC/gateway) podem ser produtivos.

Frameworks de injeção de dependência

  • Frameworks de DI leves (por exemplo, fx) são elogiados por lidar com grafos complexos de inicialização/desligamento.
  • Céticos argumentam que frameworks de DI adicionam indirection; eles preferem construção explícita em main, alegando que grafos acíclicos e teardown manual continuam gerenciáveis.