GO: Como organizar meu código?

Se você se sente perdido(a) na estrutura de pastas em Golang, esse artigo é pra você!

Muito se fala sobre designs de projeto mas pouco se aplica na realidade. Por exemplo, seguir o Domain-Driven Design à risca não é uma tarefa fácil. Sua ideia até foi moldada de forma abstrata, mas, quando uma linguagem traz limitações estruturais, isso aumenta a dificuldade de aprendizado.

Golang é esse tipo de linguagem: toda vez que você pensa que está fazendo algo prático, alguma hora depois irá descobrir que algo ficou redundante a ponto de prejudicar a legibilidade do código ou deixar o esquema de importações bastante confuso. Eu passei muito por isso, rs.

Eu fui aprendendo muita coisa com o tempo, absorvendo conteúdos de muitas pessoas. Dito isso, eu não sei a quem dar créditos por cada pequeno conhecimento que me trouxe ao ponto que cheguei. Vou evitar atribuir nomes aos bois, ok? Mas prometo colocar alguns artigos ao final deste, pode ser?!

Vamos começar pelo começo.

Iniciando um projeto em Go

Se você pesquisar um pouco, vai ver que nem precisa de pastas para fazer um projeto Golang. Você pode simplesmente despejar quantos arquivos .go quiser na raiz e deixar a desorganização tomar conta.

OK, pera, tem projetos que isso até faz sentido. São bibliotecas (libs), projetos menores, que não costumam ter mais de 2 arquivos (o principal e o de teste). Você não vai criar todo um sistema de pastas pra colocar dois arquivos, certo??

Todavia, se o seu projeto é um pouquinho maior que isso, faz sentido repensar sobre a estrutura utilizada.

Como você organiza seus projetos, Kiko?

Primeiramente, gosto de pensar que toda aplicação Go é um recurso CLI, mesmo se o objetivo for rodar um servidor web. Dito isso, a primeira pasta que me vem em mente tem como função "organizar os comandos que irei disponibilizar para o CLI", ou seja, a pasta "cmd" (abreviação de command).

A ideia dessa pasta é expor, por exemplo, a execução principal do código (vulgo main.go) e, quando aplicável, os comandos abaixo dele, como nos exemplos abaixo:

  • myapp serve -p 80 => chama o arquivo cmd/main.go, que inicializa o servidor na porta 80, importando alguma execução em cmd/serve.go;

  • myapp cron:reset-quotas => chama o arquivo cmd/main.go, que inicializa o fluxo de Cronjobs, importando alguma execução em cmd/cron.go;

  • myapp db:migrate => chama o arquivo cmd/main.go, que inicializa o arquivo de migrations do banco em cmd/migrate.go;

  • myapp db:revert (...);

  • myapp db:seed (...);

  • e por aí vai.

É muito importante refletir quantos comandos você quer expor no terminal e quais opções de controle você quer fornecer antes de começar o projeto, pra validar a real necessidade da pasta. De repente, ter só um comando desfaz a necessidade de criar isso e você pode deixar o main.go na raiz da aplicação. É algo a se pensar, certo?

Após isso, a próxima pasta que me vem em mente e que é obrigatória a quase todos os projetos é a pasta internal.

Que pasta é essa, Kiko?

A ideia dessa pasta é separar o código público do código interno do projeto (daí o nome). Isso não significa que o código será secreto nem nada do tipo, só que outros projetos não conseguirão importar nada que estiver dentro dessa pasta. O compilador do Golang não permite a importação de pastas dentro de alguma pasta com o nome internal.

Não é sobre guardar segredos, mas de impedir que alguém use sua biblioteca da forma errada.

É qualidade de código!

E igualmente serve para centralizar a sua camada de domínio. Gosto de dizer que "o que é exclusivo do projeto, fica na pasta internal". Implicitamente, isso também significa que "o que não é exclusivo do projeto, fica fora".

Se não for comando e não for exclusivo do projeto, fica onde então, Kiko?

Re: na pasta pkg!

"pkg" é uma abreviação para "package" e é lá que você vai colocar a parte pública da sua aplicação que não está em fluxo de execuções. Tem algumas pastas que vejo ter quase como um padrão em todos os projetos que já trabalhei:

  • pkg/platform: todas as lógicas de plataforma usadas pela aplicação. Clients, Middlewares, Autenticação, etc. Sinceramente, isso poderia estar em um kit-repository e ter as "configurações" (ou divergências) importadas por variáveis de ambiente, mas isso é outro assunto;

  • pkg/util: são funções "auxiliares" que podem ser facilmente confundidas com "serializers". A grande maioria apenas transforma dados e são importadas tanto pelos pacotes internos quanto os de comando;

  • pkg/services: quando você precisa desenvolver um serviço de terceiros, quando não há um SDK ou algo do gênero para usar, você acaba colocando aqui, focando em criar apenas o serviço, sem embutir regra de negócio da sua aplicação;

  • etc.

Só com isso você já consegue organizar um projeto tranquilo, mas preciso te alertar sobre outras pastas:

  • docs: se você está fazendo uma aplicação HTTP, é comum usar gerador de documentações que extraem informação de godoc para criar uma OpenAPI (vulgo Swagger). A documentação compilada geralmente é exportada na pasta docs nos projetos em Golang. É tipo um padrão que foi instalado silenciosamente;

  • mocks: outra pasta bastante comum pra quem usa mockery. Basicamente, se você faz interface pra tudo, você consegue compilar todas as interfaces em mocks para fazer testes unitários bem refinados. Na minha humilde opinião, Golang foi a linguagem que achei estupidamente mais fácil de mockar qualquer coisa... E olha que eu já fiz todo tipo de teste automatizado aqui, hein?

  • vendor: se você usa o comando go mod vendor, o Golang irá baixar todas as suas dependências e colocar na pasta vendor. Quase toda linguagem provê esse tipo de pasta (no Node, seria a node_modules. No PHP com Composer, é vendor também, etc), então já é de senso comum não mexer em nada que estiver nela. É importado automaticamente, então não é SEU.

E agora sim, tendo noção dessas pastas, você pode começar a investigar melhor os códigos que encontrar por aí. Ainda pode haver outras pastas mas, sinceramente, isso pra mim é o necessário. Você pode ler mais sobre isso em um dos links no final desse artigo.

Desenvolvendo

Um detalhe que os iniciantes se batem em Golang é sobre como nomear e importar projetos. Eventualmente, percebe-se que há uma certa simplicidade nisso: tudo é baseado no que está definido no go.mod. Quando você batiza o nome de uma aplicação, você deve ter em mente que, se ela for ser importada em outros projetos, toda a navegação de diretórios será inciada a partir da raiz, onde fica o go.mod.

Por exemplo, se eu crio uma pasta vazia e rodo o comando no terminal go mod init myproject, o compilador da linguagem vai criar os arquivos go.mod e go.sum na raiz do projeto chamando-o myproject. Assim, quando você importar alguma função myproject.Blablabla, ele vai procurar nesse diretório-raiz.

Já os pacotes, que são outras pastas, não mencionam o nome do projeto mais. Portanto, se você repetir nomes de pastas, eventualmente você vai ter de gerar apelidos (traduzindo alias) pra não dar conflitos nas importações.

O ideal é que você jamais repita nomes de pacotes, interfaces, nada. Evite repetir qualquer coisa. Outro ponto é que todas as pastas devem ser escritas em letras minúsculas e somente letras. Nada de underline, etc. É pra dar nomes pequenos mesmo, sabe?

Explorando um pouco da estrutura de pastas que discutimos, alguns dos pacotes que vejo em comum nos projetos são:

  • internal/entity: fica a implementação de entidades de domínio, assim como algumas regras de negócio (as que forem independentes). Evite ao máximo importar outros pacotes dentro desse;

  • internal/db/model: fica a implementação de modelagem de banco de dados referente as entidades criadas anteriormente, deixando claro que o pacote entity deve evitar ao máximo mencionar qualquer coisa de banco salvo algumas exceções como dados anuláveis (ex: sql.NullTime). Na Model você geralmente descreve o nome da tabela de cada entidade e campos adicionais que só importam para o banco (ex: DeletedAt é um campo geralmente utilizado para soft-deleting ~ não é consumido pela sua aplicação, apenas pro banco, e raramente há necessidade de expor essa informação. Nesse cenário, você não coloca esse campo na entidade, somente no modelo do banco). Outro detalhe é que cada Model implementa suas funções de conversão aqui, como NewBlablablaModel() recebendo entidade e retornando model e model.ToEntity(), convertendo a modelagem para entidade;

  • internal/db/repository: aqui fica a implementação de comunicação com banco, encapsulando o que quer que use para se comunicar com banco em funções mais simplificadas pra sua aplicação. Note que há variações gigantescas de como implementar esse pacote. Algumas pessoas preferem centralizar tudo em uma interface de Repository e uma implementação que serve para todas as entidades e outras preferem ter um repository pra cada entidade. No geral, o objetivo aqui é dar aos demais pacotes algo que possa receber ou prover entidades sem expor as modelagens do banco;

  • internal/usecases/<fluxo>: como diz o nome, nesses pacotes implementamos os casos de uso da aplicação, independente de qual protocolo esteja sendo usado. A ideia é que todos os fluxos nesse pacote não saibam o que é uma requisição web ou comando CLI. Eles vão ter seus argumentos completamente restritos para somente executar o caso de uso e retornar o que precisar retornar ou erro. Um exemplo simples seria casos de uso de uma loja online, onde poderia ter os pacotes de CRUD product, category, customer, etc, e pacotes de fluxo de compra como shopping, publishing, etc. A forma como você vai separar os fluxos só depende de você. O importante é não receber *http.Request aqui dentro, muito menos http.ResponseWriter;

  • internal/http: essa pasta aqui é um verdadeiro dilema. Na minha opinião, isso poderia ser uma pasta lá no pacote cmd. Porém também é uma configuração interna da aplicação, então faz total sentido estar na internal. Então minha conclusão é mais simples: siga seu coração.

Um rapaz loiro imitando o personagem de Yu-Gi-OH, puxando uma carta invisível, de mentira, de um dos seus braços, simulando a confiança no coração das cartas.

Entendedores entenderão (Yu-Gi-OH).
Mais detalhes sobre o pacote http
No pacote http, nós colocamos tudo relacionado a inicialização da API. Por exemplo, geralmente temos um arquivo router.go com toda a definição de URLs da aplicação, incluindo path, middlewares e handlers. Não se preocupe se não entender esses termos, porém não vai dar pra explicar aqui nesse artigo. Mas, resumindo, esse pacote é o que diz pra aplicação "toda requisição GET /admin/nao-manda-em-mim deve ser tratada pela função adminHandler.Playground()". A depender do framework, essa função pode estar em um subpacote ou atrelada a uma struct. Nesse caso aqui, realmente depende de como o servidor está sendo inicializado, pois alguns frameworks criaram umas limitações bem bizarras.

Implementando esses pacotes, teu serviço estará praticamente pronto, restando somente configurar as coisas no pacote cmd e rodar.

Ah, Kiko, não vai dar nenhum exemplo de código??

Quem disse que não? Toma aí: https://github.com/kaiquegarcia/gomarket/

Eu fiz esse projeto para explorar alguns conceitos na minha mente. Por exemplo, o armazenamento em disco, gerenciado de forma abstrata por uma collection. Funcionou perfeitamente nos primeiros experimentos e deu até pra escrever testes de ponta.

Como pode ver no git, praticamente toda struct implementa uma interface. Isso serve para que o mockery consiga mockar absolutamente qualquer coisa da minha aplicação, facilitando a escrita de quaisquer testes.

Além disso, a clara separação de responsabilidades por pacotes deixa claro onde poderia estar alguma coisa. Nesse projeto, eu coloquei a pasta http dentro da cmd. Como falei: siga seu coração! E dessa vez eu quis jogar lá mesmo. Mas também tenho projetos com a presença do pacote dentro da internal. É meio louco isso, né?


Enfim, por hoje é só! Diferentemente do meu blog pessoal, aqui, no Code Aspiras, não pretendo dar continuidade de um artigo pro outro. Nesse escrevi sobre Golang, no próximo pode ser um Typescript, Angular, Javascript, PHP, Java, sei lá. Não estou com um foco em mente e ultimamente não sobra muito tempo pra fazer um trabalho mais prolongado nas escritas.

Então vou escrever o que percebo que poderia ser relevante pra quem fosse trabalhar comigo, por exemplo. Esse artigo é extremamente relevante pra qualquer iniciante de Golang que for se aventurar nos meus projetos.

Outro detalhe é que Go é uma linguagem muito nova. Não existe nada como regras absolutas. O que eu disse aqui é apenas uma das milhares de formas de se fazer um projeto bem limpo, legível e fácil de manter. Ainda há muitos assuntos que não foram discutidos, como conceitos de redução de código (DRY, AHA, etc) e técnicas para enxugar complexidades lógicas. Pretendo escrever artigos sobre eles, voltados a Golang, assim como redigir alguns aprendizados importantes que tive com outros colegas de equipe.

Então assina aí a Newsletter do CodeAspiras pra receber e-mail quando sair artigo novo, beleza? Também deixo um convite aqui pra quem quiser publicar seus artigos aqui no blog, só falar comigo que eu te mando o convite de colaborador. O processo é bem simples: você escreve e manda para aprovação. Outra pessoa vai avaliar a veracidade da informação, se o tema bate com o que discutimos, se tem erros de grafia que não são apenas conotativos, etc. Estando tudo certo, será publicado em seu nome. Não vamos mudar autor de nada. Pode vir, pode entrar, está tranquilo por aqui!

Artigos interessantes

E agora eu vou indo nessa. Abraço!

Crianças correndo para se abraçar.

Inté!