Go + Architecture Linter

Mantendo a arquitetura — Artigo 14

John Fercher
6 min readAug 8, 2024

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.

Imagem 1 — Grafo de dependencias.

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.

Código 1 — Injeção de dependencia.

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

Imagem 2 — Golang Arch Linter (retirado do github).

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.

Código 2 — Definição de componentes.

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.

Código 3 — Dependências 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:

  1. Podemos excluir arquivos da análise;
  2. Podemos alterar o nível de análise de dependências;
Código 4 — Customizações.

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.

Código 5 — Resultado deepScan.

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.

Referências

--

--

John Fercher

Tech lead at @MercadoLibre, gamer, master of science and open source contributor. More about: johnfercher.com