Go + Architecture Linter
Mantendo a arquitetura — Artigo 14
Postman collection, código do artigo, diff com o artigo 13.
Sumário: Desenvolvimento de APIs em Go
Próximo Artigo: Go + gRPC — Utilizando protobuf
Artigo Anterior: Go + Arquitetura Hexagonal — Isolando o domínio e outras coisas mais
Introdução
Depois de aplicarmos a Arquitetura Hexagonal em nosso projeto, chegou a hora de garantirmos que a estrutura proposta está, de fato, sendo respeitada. Para realizar essa tarefa, utilizaremos o projeto go-arch-lint.
Antes de explicar como usar o go-arch-lint, vamos dar uma olhada em uma funcionalidade muito interessante dele: a geração de grafos de dependências. Vamos espiar o que vem por aí. Na imagem 1, podemos ver como ficou a API do último artigo.
Na imagem 1, vemos que o grafo de dependências está exatamente como gostaríamos. Vamos fazer uma breve análise.
1 — Models não depende de nada
Isso faz todo o sentido, pois os modelos que representam as entidades ligadas ao core não devem depender de nada.
2 — Ports só depende de models
Novamente, isso faz todo o sentido. As portas, que são as abstrações de como o core interage com o mundo exterior, devem levar em consideração apenas os modelos do core.
3— Services depende de models e ports
Aqui temos uma situação interessante. Os serviços são as implementações das portas, e neles vamos interagir com outras implementações de portas, sejam elas outros serviços ou repositories. Dito isso, por que não existem dependências de services para drivens aqui?
A resposta é: Utilizamos injeção de dependência via construtor, onde só dependemos de interfaces e não de structs.
4 — Drivens depende de models e utiliza a lib GORM
Como temos uma única implementação de uma porta driven, que é nosso repositório MySQL, isso explica por que o pacote depende da biblioteca GORM (ORM para Go). Nesse ponto, não preciso mais explicar a razão de depender de models.
5 — Drivers depende de models, ports, api e utiliza a lib chi
Para não me repetir, vou pular a explicação sobre a dependência em models e ports. As outras dependências que temos aqui referem-se ao pacote de API, onde temos alguns objetos genéricos relacionados a respostas HTTP. E a dependência da biblioteca Chi faz todo sentido, pois é nosso framework para levantar os endpoints.
Finalizando essa breve análise, vamos entender como gerar esse belo grafo e como aplicar as regras de dependência no projeto.
Go-Arch-Lint
O go-arch-lint é um programa open-source escrito em Go sob a licença MIT. Ele serve para analisar as dependências do seu código e possui uma funcionalidade que permite criar um arquivo YAML na raiz do projeto com regras de dependência.
Para ver todas as regras e a sintaxe que deve ser utilizada, clique aqui. Neste artigo, vou explicar o básico necessário para aplicar o linter na sua API. Mas aqui vem uma pergunta: ele funciona apenas para a Arquitetura Hexagonal?
A resposta é: Não
Esse projeto se baseia nas dependências e referências geradas entre os pacotes do Go. É possível aplicar o linter a qualquer arquitetura; basta conhecer as regras a serem aplicadas e escrever o arquivo .go-arch-lint.yml
na raiz do projeto, utilizando a sintaxe correta.
Definindo os Pacotes
O projeto faz distinção entre components e vendors. Os componentes são os pacotes criados dentro do projeto em Go, enquanto os vendors são as bibliotecas importadas na sua aplicação. Nessa etapa, é necessário mapear todos os pacotes da API, como pode ser visto no código 2 a seguir.
No código 2, mapeamos primeiro nossos pacotes relacionados à Arquitetura Hexagonal (models, ports, drivers, drivens e services). Vale notar que estamos utilizando wildcards /**
no final do caminho; isso indica que tudo que estiver dentro daquele diretório será considerado um único pacote.
Após isso, mapeamos o restante dos pacotes da API. Não é necessário realizar um mapeamento muito minucioso nos outros diretórios, pois as regras não são tão restritas para o restante da aplicação (a menos que você queira). Por exemplo, nosso pacote pkg
contém muitos elementos, mas se qualquer coisa desse pacote for importada no core, já estará errado. Esse é o ponto mais importante.
Ainda no código 2, depois de mapear todos os componentes, vamos mapear os vendors. Aqui, fazemos uma lista de todas as bibliotecas que utilizamos. Por último, definimos nossos commonVendors
, que são aquelas bibliotecas que todos os componentes da nossa aplicação podem utilizar. Aqui, mapeei os pacotes de testes (assert e mock) e o pacote de trabalho com UUID.
Definindo as Dependências
Para definir as dependências, utilizamos os componentes e os vendors definidos na etapa anterior. A ideia aqui é especificar quais bibliotecas um pacote pode utilizar (canUse) e quais componentes o pacote pode depender (mayDependOn). O código 3 contém todas as relações permitidas.
Mais Definições
O go-arch-lint também possui algumas outras funções importantes na sintaxe, que podem ser aplicadas em projetos com base em necessidades específicas. Algumas dessas funcionalidades são:
- Podemos excluir arquivos da análise;
- Podemos alterar o nível de análise de dependências;
No exemplo do código 4, estamos excluindo os arquivos de testes, além dos arquivos api.go
e internal/internal.go
, que servem apenas como marcadores para o mockery gerar mocks.
A opção allow.deepScan
, definida como false, serve para não inferir os relacionamentos entre structs e interfaces. Se essa opção estiver ativada, o projeto passa a considerar a implementação de uma interface como parte da interface. Dessa forma, ele pode considerar algo como apresentado no código 5.
Como mostrado no código 5, o linter passa a encontrar uma dependência entre drivens e services, porque ele entende que services depende da struct e não de ports.
Utilizando o Linter
Para instalar o projeto, basta executar o comando mostrado a seguir.
go install github.com/fe3dback/go-arch-lint@latest
# opcional para instalar globalmente
sudo cp $GOPATH/bin/go-arch-lint /usr/local/bin/
Para rodar o linter, depois de ter o arquivo .go-arch-lint.yml
criado e configurado na raiz do projeto, basta executar o comando a seguir:
go-arch-lint check
O comando apresentando anteriormente valida todas as regras descritas no arquivo de configuração, se alguma dependência for criada fora da regra, o linter aponta os problemas e retorna um código de erro, que pode ser utilizado para quebrar CIs.
Para gerar o grafo de dependências, basta executar o comando:
go-arch-lint graph --include-vendors
O projeto é bastante completo. Se você precisar criar um pre-commit, também é possível. Basta definir o hook como:
Para finalizar as funcionalidades do go-arch-lint, o projeto também possui um plugin para GoLand que está em desenvolvimento.
Conclusões e Próximas Etapas
Neste artigo, vimos como utilizar o projeto go-arch-lint para aplicar um linter em sua aplicação. O projeto é muito útil para garantir que as regras de sua arquitetura estão sendo seguidas.
Por fim, o projeto também adotou o caminho de analisar as dependências entre pacotes e possibilitar a criação de regras customizadas. Com isso, podemos criar regras para várias arquiteturas, como Arquitetura Limpa, MVC, MVVM, entre outras.
No próximo artigo, vamos ver como criar endpoints gRPC para nossa API. Esse tópico ainda está conectado ao tema da arquitetura, pois serão criados novos adaptadores driver e vamos acrescentar um novo vendor que deverá ser incluído nos adapters.