logo hsb.horse
← Voltar para o índice do blog

Blog

Política básica ao implementar um servidor API com Golang

Organizei minhas próprias políticas básicas ao escrever um servidor API em Go, incluindo binário único + cobra, foco na operação, separação HTTP/CLI, tratamento seguro de erros e desligamento normal.

Publicado:

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

ArtigoPolítica
Produto finalComece com um único binário de cobra
Iniciar servidor HTTPComece com o subcomando serve como myapp serve
Unidade centralCentro Operation[In, Out]
transporteHTTP e CLI são separados como adaptadores
quadroNão vaze chi / gin / Echo fora do transporte/http
corte transversalMiddleware HTTP e interceptador de operação são separados
erroTraduzir com segurança erros internos com limites
desligamentoEm 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.

Terminal window
./myapp serve
./myapp health
./myapp users get 123
./myapp users create --name alice

Acho 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.

  1. Apenas uma apostila é necessária
  2. 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.
  3. Fácil de compartilhar bootstrap entre servidor HTTP e CLI
  4. 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.

CamadaO que colocar
Middleware HTTPCoisas específicas de HTTP, como ID de solicitação, log de acesso, recuperação, autenticação, CORS
Interceptador de operaçãovalidaçã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.

ArtigoFunção
CodeClassificações como bad_request e not_found
PublicRedação que pode ser devolvida ao cliente
InternalErro interno detalhado
MetaLogs 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.

EncomendarCoisas para fazer
1Receber sinal de terminação em signal.NotifyContext
2Interrompa a nova entrada descartando readiness
3Fechar a fila de entrada do consumidor / cron / trabalhador em segundo plano
4drain por um tempo e aguarde o balanceador de carga ser refletido
5Propagar cancelamento de contexto para manipulador
6Ligue para server.Shutdown(timeoutCtx)
7Aguarde a conclusão do trabalho em voo em WaitGroup / errgroup
8Finalmente feche o produtor de banco de dados/cache/broker
9Use 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.

NomeSignificado
myapp healthCLI para diagnosticar dependências diretamente localmente
GET /api/healthEndpoint 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.

Referência