Ao começar a escrever um servidor API em Go, você deseja primeiro escolher um roteador ou estrutura. Mas não é isso que você realmente deve decidir primeiro. As primeiras coisas que você deseja corrigir são o modelo de execução, a divisão de responsabilidades, os limites de erros e a ordem de parada.
O roteador pode ser alterado posteriormente. Um projeto com uma ordem de parada interrompida é difícil de consertar posteriormente. Acho que seria mais fácil consertar isso primeiro.
Este artigo não é uma teoria geral do Go, mas sim um memorando de política básico para ajudá-lo a evitar confusão na próxima vez que implementar um servidor API.
Corrija a política primeiro
| Artigo | Política |
|---|---|
| Produto final | Comece com um único binário de cobra |
| Iniciar servidor HTTP | Comece com o subcomando serve como myapp serve |
| Unidade central | Centro Operation[In, Out] |
| transporte | HTTP e CLI são separados como adaptadores |
| quadro | Não vaze chi / gin / Echo fora do transporte/http |
| corte transversal | Middleware HTTP e interceptador de operação são separados |
| erro | Traduzir com segurança erros internos com limites |
| desligamento | Em vez de apenas chamar server.Shutdown, defina a ordem de desligamento como deterministic |
Resumindo, a estrutura não está centrada na estrutura HTTP, mas sim nas operações e nos ciclos de vida.
O servidor HTTP também é tratado como CLI
O servidor HTTP é realmente iniciado usando a CLI. Nesse caso, seria mais fácil de entender se você os agrupasse em cobra desde o início e os combinasse em um binário.
O sistema de comando público recomendado é o seguinte.
./myapp serve./myapp health./myapp users get 123./myapp users create --name aliceAcho que uma abordagem em duas etapas como server run é redundante. serve completa o significado e reduz a quantidade de digitação.
A razão para favorecer o binário único é simples.
- Apenas uma apostila é necessária
- A leitura da configuração, o registrador, a inicialização do banco de dados e o tratamento de erros podem ser feitos em um sistema.
- Fácil de compartilhar bootstrap entre servidor HTTP e CLI
- Mesmo se você quiser converter para 2 binarização posteriormente, você pode apenas aumentar ligeiramente
cmd/.
Por outro lado, a escolha de binários separados desde o início só pode ser feita depois de você entender o motivo, como querer tornar o contêiner do servidor extremamente pequeno, ter dependências e privilégios significativamente diferentes entre o servidor e o cli ou querer separar a frequência de lançamentos.
O núcleo da implementação está em Operation
Se você projetar em torno do manipulador HTTP, a conveniência dos roteadores e estruturas estará profundamente envolvida. Então, quando você deseja estender a CLI, tende a escrever o mesmo processo novamente.
É melhor centralizá-lo em Operation.
type Operation[In any, Out any] interface { Execute(ctx context.Context, in In) (Out, error)}Seja users get ou users create, basta chamar a mesma operação para HTTP e CLI. HTTP é um adaptador que converte solicitação/resposta e CLI é um adaptador que converte flag/stdin/stdout.
Se você fizer isso, o núcleo do aplicativo não mudará, mesmo se você aumentar o número de transportes.
A estrutura está confinada a transport/http
Usar chi, gin ou Echo é uma questão de transporte. Não se trata de domínios ou operações.
O que você deseja fazer ao escolher um roteador não é decidir a responsabilidade do servidor API, mas apenas decidir como usar o HTTP. Portanto, as diferenças de estrutura estão confinadas em internal/transport/http.
A imagem fica assim.
internal/ bootstrap/ operation/ interceptor/ transport/ http/ cli/ domain/ infra/ apperr/Se você vir chi ou gin.Context de operation, provavelmente está vazando. Se isso começar a vazar, não é uma questão de mudar o framework, é uma questão de reescrever todo o aplicativo.
Não misture middleware HTTP e interceptador de operação
Será mais fácil pensar nisso separadamente.
| Camada | O que colocar |
|---|---|
| Middleware HTTP | Coisas específicas de HTTP, como ID de solicitação, log de acesso, recuperação, autenticação, CORS |
| Interceptador de operação | validação, autorização, tempo limite, métricas, registro por caso de uso |
O middleware HTTP é conveniente para transporte. O interceptador de operação é para conveniência do aplicativo. Mesmo com o mesmo registro, o significado do local é diferente.
A combinação desses dois tende a resultar em um design fechado em HTTP. No momento em que você adicionar a CLI, você terá que reescrever a validação e a autenticação com implementações separadas.
Converta erros com segurança com limite
Não gere erros internos diretamente na resposta HTTP ou na saída padrão da CLI. É mais fácil estabelecer regras aqui desde o início.
No seu aplicativo, use um erro que tenha pelo menos os quatro elementos a seguir.
| Artigo | Função |
|---|---|
Code | Classificações como bad_request e not_found |
Public | Redação que pode ser devolvida ao cliente |
Internal | Erro interno detalhado |
Meta | Logs e informações auxiliares |
Em HTTP, isso se traduz em código de status e JSON. Na CLI, isso se traduz em stderr e código de saída. O importante é fazer a translação na fronteira. Não comece a pensar no status do HTTP no lado da operação.
O desligamento normal deve ser projetado incluindo a ordem de desligamento
Este é um ponto muito importante na implementação do servidor API do Go. Ele não é tratado como “desligamento normal suportado” apenas chamando srv.Shutdown(ctx).
Na documentação oficial net/http, Shutdown fecha o ouvinte, interrompe a nova recepção, fecha a conexão inativa e aguarda que a conexão ativa retorne ao estado inativo. Por outro lado, ListenAndServe retorna ErrServerClosed após chamar Shutdown ou Close. Em outras palavras, se você finalizar main apenas olhando o retorno, o processo poderá terminar enquanto estiver parado.
Além disso, Shutdown não espera por conexões sequestradas. Use RegisterOnShutdown se necessário e use BaseContext se desejar propagar o desligamento para o lado do manipulador. Se você converter o sinal em um contexto usando os/signal’s NotifyContext, será mais fácil alinhar o ponto inicial para o processamento de terminação.
Se você estiver usando Kubernetes, a ordem de parada é ainda mais importante. Ciclo de vida do pod dá ao pod um período de encerramento normal e ganchos do ciclo de vida do contêiner continua sua contagem regressiva enquanto executa PreStop. Portanto, se você parar de maneira descuidada, o tempo acabará antes que drenar termine.
Minha política básica é esta ordem.
| Encomendar | Coisas para fazer |
|---|---|
| 1 | Receber sinal de terminação em signal.NotifyContext |
| 2 | Interrompa a nova entrada descartando readiness |
| 3 | Fechar a fila de entrada do consumidor / cron / trabalhador em segundo plano |
| 4 | drain por um tempo e aguarde o balanceador de carga ser refletido |
| 5 | Propagar cancelamento de contexto para manipulador |
| 6 | Ligue para server.Shutdown(timeoutCtx) |
| 7 | Aguarde a conclusão do trabalho em voo em WaitGroup / errgroup |
| 8 | Finalmente feche o produtor de banco de dados/cache/broker |
| 9 | Use server.Close() como último recurso somente quando o tempo limite for excedido |
Acho que o cerne desta discussão não é tanto específico do Go, mas sim que você deve projetar suas sequências de parada para serem determinísticas.
Existem dois tipos de health
É fácil ficar confuso aqui também.
| Nome | Significado |
|---|---|
myapp health | CLI para diagnosticar dependências diretamente localmente |
GET /api/health | Endpoint de integridade HTTP para iniciar o servidor |
O mesmo health, mas funções diferentes. O lado CLI é semelhante ao diagnóstico e o lado HTTP é semelhante a readiness / liveness. Em particular, é mais seguro projetar a integridade do lado HTTP para retornar 503 durante o desligamento.
Política de diretório mínimo
Isso é suficiente para o esqueleto inicial.
cmd/ myapp/internal/ apperr/ bootstrap/ domain/ infra/ interceptor/ operation/ health/ users/ transport/ cli/ http/ version/Mantenha cmd/myapp magro. A inicialização vai para bootstrap, os casos de uso para operation e HTTP e CLI para transport. Mesmo se você quiser converter para binarização 2 posteriormente, se usar este método de corte, poderá facilmente adicionar cmd/myapp-server.
Frase que você deseja manter
Ao implementar um servidor Go API, a primeira coisa a decidir não é a estrutura, mas o modelo de execução, divisão de responsabilidades, limites de erro e ordem de parada.
Esta frase geralmente é suficiente. Você pode decidir se deseja usar chi ou Echo depois disso.
resumo
Minha política básica é esta. Primeiro, combine-o em um único binário de cobra e inicie o servidor HTTP com serve. O centro será colocado em Operation e HTTP e CLI serão separados como adaptadores. A estrutura está confinada a transport/http. O erro é convertido com segurança usando limite. O desligamento não termina em server.Shutdown, mas o pedido foi projetado para incluir prontidão, drenagem, parada do trabalhador e limpeza de recursos.
Se você fizer isso, o design permanecerá consistente mesmo se você alterar a estrutura posteriormente, adicionar uma CLI ou aumentar o número de trabalhadores.
hsb.horse