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çãorun(), 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.goem “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.