Estrutura da API Golang

No processo de conhecer Golang, decidi criar a estrutura do aplicativo, que seria conveniente para eu trabalhar no futuro. O resultado foi, na minha opinião, uma boa peça de trabalho, que decidi compartilhar e, ao mesmo tempo, discutir os momentos que surgiram durante a criação do quadro.


imagem


Em princípio, o design da linguagem Go sugere que ele não precisa criar aplicativos em larga escala (quero dizer a falta de genéricos e um mecanismo de manipulação de erros não muito poderoso). Mas ainda sabemos que o tamanho dos aplicativos geralmente não diminui, mas mais frequentemente pelo contrário. Portanto, é melhor criar imediatamente uma estrutura na qual será possível criar novas funções sem sacrificar o suporte ao código.


Tentei inserir menos código no artigo. Em vez disso, adicionei links para linhas de código específicas no Github, na esperança de que fosse mais conveniente ver a imagem inteira.


Primeiro, esbocei um plano para o que deveria estar no aplicativo. Como no artigo falarei sobre cada item separadamente, primeiro darei o principal desta lista como o conteúdo.


  • Escolha o gerenciador de pacotes
  • Escolha uma estrutura para criar uma API
  • Selecionar ferramenta para injeção de dependência (DI)
  • Rotas de solicitação da Web
  • Respostas JSON / XML de acordo com os cabeçalhos da solicitação
  • ORM
  • Migrações
  • Criar classes base para as camadas do modelo Serviço-> Repositório-> Entidade
  • Repositório básico CRUD
  • Serviço CRUD básico
  • Controlador CRUD básico
  • Solicitar validação
  • Configurações e variáveis ​​de ambiente
  • Comandos do console
  • Registo
  • Integração do criador de logs com o Sentry ou outro sistema de alerta
  • Configurando alerta para erros
  • Testes unitários com redefinição de serviços através do DI
  • Mapa de código de porcentagem e cobertura de teste
  • Swagger
  • Docker compor

Gerenciador de pacotes


Depois de ler as descrições para várias implementações, escolhi o govendor e, no momento, fiquei satisfeito com a escolha. O motivo é simples - permite instalar dependências dentro do diretório com o aplicativo, armazenar informações sobre pacotes e suas versões.


Informações sobre pacotes e suas versões são armazenadas em um arquivo vendor.json. Há um ponto negativo nessa abordagem também. Se você adicionar um pacote com suas dependências, juntamente com informações sobre o pacote, informações sobre suas dependências também serão inseridas no arquivo. O arquivo cresce rapidamente e não é mais possível determinar claramente quais dependências são as principais e quais são derivadas.


No compositor PHP ou no npm, as principais dependências são descritas em um arquivo, e todas as dependências principais e derivadas e suas versões são automaticamente registradas no arquivo de bloqueio. Essa abordagem é mais conveniente na minha opinião. Mas, por enquanto, a implementação do fornecedor foi suficiente para mim.


Enquadramento


Na estrutura, não preciso de muito, um roteador conveniente, validação de solicitações. Tudo isso foi encontrado no popular Gin . Ele parou nisso.


Injeção de dependência


Com o DI, tive que sofrer um pouco. Primeiro, escolha Dig. E no começo tudo foi ótimo. Serviços descritos, Dig ainda cria dependências, convenientemente. Porém, os serviços não podem ser redefinidos, por exemplo, durante o teste. Portanto, no final, cheguei à conclusão de que peguei um contêiner de serviço simples sarulabs / di .


Eu apenas tive que fazer uma bifurcação, porque pronto para adicionar serviços e proíbe redefini-los. E, ao escrever autotestes, na minha opinião, é mais conveniente inicializar o contêiner como no aplicativo e redefinir alguns dos serviços, especificando stubs. No fork, ele adicionou um método para substituir a descrição do serviço.


Mas no final, tanto no caso do Dig quanto no contêiner de serviço, tive que colocar os testes em um pacote separado. Caso contrário, verifica-se que os testes são executados separadamente em pacotes ( go test model/service ), mas não são iniciados imediatamente para todo o aplicativo ( go test ./... ), devido às dependências cíclicas que surgem nesse caso.


Respostas JSON / XML de acordo com os cabeçalhos da solicitação


No Gin, eu não encontrei isso, então acabei de adicionar um método ao controlador base que gera uma resposta dependendo do cabeçalho da solicitação.


 func (c BaseController) response(context *gin.Context, obj interface{}, code int) { switch context.GetHeader("Accept") { case "application/xml": context.XML(code, obj) default: context.JSON(code, obj) } } 

ORM


Com ORM não sentiu o longo tormento de escolha. Havia muito por onde escolher. Mas, de acordo com a descrição dos recursos, gostei do GORM, que é um dos mais populares no momento da seleção. Há suporte para o DBMS mais comumente usado. Pelo menos o PostgreSQL e o MySQL estão definitivamente lá. Ele também possui métodos para gerenciar o esquema base que você pode usar ao criar migrações.


Migrações


Para migrações, decidi pelo pacote gorm-ganso . Coloquei um pacote separado globalmente e inicio a migração para ele. No início, essa implementação era embaraçosa, pois a conexão com o banco de dados precisava ser descrita em um arquivo db / dbconf.yml separado. Mas, então, descobriu-se que a cadeia de conexão nela pode ser descrita de tal maneira que o valor é obtido da variável de ambiente.


 development: driver: postgres open: $DB_URL 

E isso é bastante conveniente. Pelo menos com o docker-compose, não tive que duplicar a cadeia de conexão .


O Gorm-ganso também suporta reversões de migração, o que acho muito útil.


Repositório básico CRUD


Prefiro tudo o que se refere aos recursos a serem colocados em uma camada de repositório separada. Na minha opinião, com essa abordagem, o código da lógica de negócios é mais limpo. Nesse caso, o código de lógica de negócios sabe apenas que precisa trabalhar com os dados que ele obtém do repositório. E o que acontece no repositório, a lógica de negócios não é importante. O repositório pode trabalhar com um banco de dados relacional, com um armazenamento KV, com um disco ou talvez com a API de outro serviço. O código da lógica de negócios será o mesmo em todos esses casos.


O repositório CRUD implementa a seguinte interface


 type CrudRepositoryInterface interface { BaseRepositoryInterface GetModel() (entity.InterfaceEntity) Find(id uint) (entity.InterfaceEntity, error) List(parameters ListParametersInterface) (entity.InterfaceEntity, error) Create(item entity.InterfaceEntity) entity.InterfaceEntity Update(item entity.InterfaceEntity) entity.InterfaceEntity Delete(id uint) error } 

Ou seja, o CRUD implementa as operações Create() , Find() , List() , Update() , Delete() e o método GetModel() .


Sobre GetModel () . Existe um repositório básico do CrudRepository que implementa operações básicas do CRUD. Nos repositórios que o incorporam, basta indicar com qual modelo eles devem trabalhar. Para fazer isso, o método GetModel() deve retornar um modelo GORM. Então tivemos que usar o resultado de GetModel() usando reflexão nos métodos CRUD.


Por exemplo


 func (c CrudRepository) Find(id uint) (entity.InterfaceEntity, error) { item := reflect.New(reflect.TypeOf(c.GetModel()).Elem()).Interface() err := c.db.First(item, id).Error return item, err } 

Ou seja, nesse caso, foi necessário abandonar a digitação estática em favor da digitação dinâmica. Nesses momentos, a falta de genéricos no idioma é sentida especialmente.


Para que os repositórios que trabalham com modelos específicos implementem suas próprias regras de filtragem de listas no método List() , primeiro implementei a ligação tardia para que o método responsável pela construção da consulta seja chamado a partir do método List() . E esse método pode ser implementado em um repositório específico. É difícil, de alguma maneira, abandonar os padrões de pensamento que foram formados quando se trabalha com outras línguas. Mas, olhando para ele com uma nova aparência e apreciando a “elegância” do caminho escolhido, ele o refez para uma abordagem mais próxima de Go. Para fazer isso, simplesmente no CrudRepository por meio da interface, é declarado um construtor de consultas , que já é List() .


 listQueryBuilder ListQueryBuilderInterface 

Acontece bem engraçado. Limitar o idioma à ligação tardia, o que a princípio parece uma falha, incentiva uma separação mais clara do código.


Serviço CRUD básico


Não há nada interessante aqui, pois não há lógica de negócios na estrutura. Chamadas de métodos CRUD para o repositório são simplesmente enviadas por proxy .


Na camada de serviços, a lógica de negócios deve ser implementada.


Controlador CRUD básico


O controlador implementa métodos CRUD . Eles processam os parâmetros da solicitação, o controle é transferido para o método de serviço correspondente e, com base na resposta do serviço, uma resposta é formada para o cliente.


Com o controlador, tive a mesma história do repositório sobre as listas de filtragem. Como resultado, refiz a implementação com ligação tardia caseira e adicionei um hidratador que, com base nos parâmetros de solicitação, forma uma estrutura com parâmetros para filtrar a lista.


No hidratador que acompanha o controlador CRUD, apenas os parâmetros de paginação são processados. Nos controladores específicos nos quais o controlador CRUD está integrado, o hidratador pode ser redefinido .


Solicitar validação


A validação é realizada por Gin. Por exemplo, ao adicionar um registro (método Create() ), basta decorar os elementos da estrutura da entidade


 Name string `binding:"required"` 

O método ShouldBindJSON() da estrutura cuida da verificação dos parâmetros de solicitação quanto à conformidade com os requisitos descritos no decorador.


Configurações e variáveis ​​de ambiente


Gostei muito da implementação do Viper , especialmente em conjunto com o Cobra.


Lendo a configuração que descrevi em main.go. Os parâmetros básicos que não contêm segredos são descritos no arquivo base.env . Você pode substituí-los no arquivo .env adicionado ao .gitignore. Em .env, você pode descrever valores secretos para o ambiente.


Variáveis ​​de ambiente têm uma prioridade mais alta.


Comandos do console


Para a descrição dos comandos do console, eu escolhi o Cobra . Do que é bom usar o Cobra junto com o Viper. Podemos descrever o comando


 serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 

E vincule a variável de ambiente ao valor do parâmetro de comando


 viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 

De fato, toda a aplicação dessa estrutura é console. O servidor da web é iniciado por um dos comandos do console do servidor.


 gin -i run server 

Registo


Eu escolhi o pacote logrus para o log , pois ele tem tudo o que eu normalmente preciso: definir níveis de log, onde registrar, adicionar ganchos, por exemplo, para enviar logs para o sistema de alerta.


Integração do criador de logs com o sistema de alerta


Eu escolhi o Sentry, porque tudo ficou bastante simples graças à pronta integração com logrus: logrus_sentry . Fiz os parâmetros com o URL para o Sentry SENTRY_DSN e o tempo limite para enviar para o Sentry SENTRY_TIMEOUT . Descobriu-se que, por padrão, o tempo limite é pequeno, se não equivocado, 300 ms e muitas mensagens não foram entregues.


Configurando alerta para erros


Fiz o processamento de pânico separadamente para o servidor da web e para os comandos do console .


Testes unitários com redefinição de serviços através do DI


Como observado acima, um pacote separado teve que ser alocado para testes de unidade. Como a biblioteca selecionada para criar um contêiner de serviço não permitia a redefinição de serviços, no fork foi adicionado um método para redefinir a descrição dos serviços. Por esse motivo, no teste de unidade, você pode usar a mesma descrição de serviços que no aplicativo


 dic.InitBuilder() 

E redefina apenas algumas descrições de serviço nos stubs dessa maneira


 dic.Builder.Set(di.Def{ Name: dic.UserRepository, Build: func(ctn di.Container) (interface{}, error) { return NewUserRepositoryMock(), nil }, }) 

Em seguida, você pode construir um contêiner e usar os serviços necessários no teste:


 dic.Container = dic.Builder.Build() userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface) 

Assim, testaremos o userService, que, em vez do repositório real, usará o stub fornecido.


Mapa de código de porcentagem e cobertura de teste
Fiquei completamente satisfeito com o utilitário de teste padrão.


Você pode executar testes individualmente


 go test test/unit/user_service_test.go -v 

Você pode executar todos os testes de uma só vez


 go test ./... -v 

Você pode criar um mapa de cobertura e calcular a porcentagem de cobertura


 go test ./... -v -coverpkg=./... -coverprofile=coverage.out 

E veja um mapa de cobertura de código com testes em um navegador


 go tool cover -html=coverage.out 

Swagger


Existe um projeto gin-swagger para o Gin, que pode ser usado para gerar especificações para o Swagger e para gerar documentação com base nele. Mas, como se viu, para gerar especificações para operações específicas, é necessário indicar comentários sobre funções específicas do controlador. Isso acabou não sendo muito conveniente para mim, pois eu não queria duplicar o código de operações CRUD em cada controlador. Em vez disso, em controladores específicos, simplesmente incorporo um controlador CRUD conforme descrito acima. Eu realmente não queria criar funções de stub para isso também.


Portanto, cheguei à conclusão de que a especificação está sendo gerada usando goswagger , porque neste caso as operações podem ser descritas sem estarem vinculadas a funções específicas .


 swagger generate spec -o doc/swagger.yml 

A propósito, com o goswagger você pode até ir pelo contrário e gerar o código do servidor da web com base na especificação do Swagger. Mas com essa abordagem, houve dificuldades com o uso do ORM, e eu finalmente o abandonei.


A documentação é gerada usando gin-swagger, para isso é indicado um arquivo de especificação pré-gerado.


Docker compor


Na estrutura, adicionei uma descrição de dois contêineres - para o código e para a base . No início do contêiner com o código, aguardamos até que o contêiner com a base seja totalmente lançado. E a cada início, lançamos migrações, se necessário. Os parâmetros para conectar-se ao banco de dados para migrações são descritos, como mencionado acima, no dbconf.yml , onde foi possível usar a variável de ambiente para transferir as configurações de conexão ao banco de dados.


Obrigado pela atenção. No processo, tive que me adaptar aos recursos do idioma. Gostaria de saber a opinião dos colegas que passaram mais tempo com o Go. Certamente alguns momentos poderiam ficar mais elegantes, por isso terei prazer em receber críticas úteis. Link para o quadro: https://github.com/zubroide/go-api-boilerplate

Source: https://habr.com/ru/post/pt455302/


All Articles