27 pontos por GN⁺ 2025-04-24 | 4 comentários | Compartilhar no WhatsApp
  • A linguagem Go proíbe estritamente referências circulares entre pacotes, o que naturalmente induz um design em camadas (layered design)
  • Este texto explica a estrutura em camadas que projetos Go acabam tendo obrigatoriamente e defende que ela já é suficientemente válida sem precisar impor outra arquitetura por cima
  • Quando surge uma dependência circular, o texto apresenta estratégias de refatoração concretas e práticas em etapas para resolvê-la
  • Cada pacote é projetado para ter uma unidade funcional com significado próprio, o que favorece testes, manutenção e separação em microsserviços
  • No fim, essa abordagem evita o problema comum no design de código de "você queria uma banana, mas acabou trazendo a selva inteira"

Abordagem de design em camadas no Go

Princípios básicos

  • O Go proíbe referências circulares entre pacotes
  • As relações de import de todo programa Go devem formar um grafo acíclico direcionado (DAG)
  • Essa estrutura não é uma escolha, mas uma regra de design imposta no nível da linguagem

Formação automática do empilhamento de pacotes

  • Excluindo os pacotes externos, os pacotes internos do projeto podem ser automaticamente organizados em camadas de acordo com a profundidade das referências
  • Como na figura abaixo, na base ficam pacotes utilitários centrais, como metrics, logging e estruturas de dados compartilhadas
  • Depois, os pacotes superiores vão combinando funcionalidades e se empilhando gradualmente para cima

Características dessa forma de projetar

  • As camadas se baseiam na direção das referências, não em abstrações hierárquicas
  • Um pacote pode referenciar vários pacotes de nível inferior
  • Abordagens clássicas como MVC e arquitetura hexagonal também podem ser aplicadas sobre essa estrutura
    → Mas é indispensável considerar as restrições estruturais do Go

Estratégias para resolver referências circulares

Quando surgir uma referência circular, tente a refatoração na ordem abaixo:

1. Mover funcionalidade

  • A abordagem mais recomendada
  • Analise com precisão a funcionalidade que causa o ciclo e mova-a para o lugar logicamente adequado
  • Não é usada com tanta frequência, mas é a que mais melhora a clareza conceitual

2. Separar a funcionalidade compartilhada em outro pacote

  • Mova tipos ou funções usados em comum pelos dois lados (Username, por exemplo) para um terceiro pacote
  • Mesmo que o pacote pareça pequeno, separe sem medo
    → Com o tempo, há grande chance de esse pacote crescer

3. Criar um pacote superior de composição

  • Crie um terceiro pacote que componha os dois pacotes em ciclo
  • Ex.: separar a dependência bidirecional entre Category e BlogPost em um pacote superior
    → Os pacotes inferiores continuam como structs simples, e a funcionalidade real é composta no pacote superior

4. Introduzir interfaces

  • Substitua a dependência por uma interface que tenha apenas os métodos necessários para a struct ou função
  • Remove dependências desnecessárias e melhora a facilidade de teste
  • Mas, se usado em excesso, isso pode acabar deixando o design mais complexo

5. Copiar

  • Se o alvo da dependência for muito pequeno, simplesmente copie e use
  • Pode parecer uma violação de DRY, mas em muitos casos isso ajuda a deixar o design mais claro

6. Unir em um único pacote

  • Se nenhum dos métodos acima for possível, mescle os dois pacotes
  • Se não virar um pacote grande demais, isso pode ser aceitável
    → Ainda assim, evite fazer essa fusão de forma automática e decida com cuidado

Vantagens práticas dessa abordagem

  • Cada pacote tem uma unidade funcional significativa por si só e pode ser testado de forma independente
  • Como as referências dentro do pacote são limitadas, é possível entender um pacote isoladamente sem precisar compreender todo o código
  • Evita ligações globais de dependência não intencionais (= o problema da selva) e incentiva a escrever código que usa apenas o necessário
  • Também é fácil extrair para microsserviços quando necessário
    → A maioria das dependências já está claramente definida

Conclusão

  • As restrições de design de pacotes no Go não são um incômodo, mas um mecanismo que induz bom design
  • Mesmo sem uma arquitetura especial, é possível implementar um design robusto apenas com a estrutura de referências entre pacotes
  • A análise cuidadosa e as estratégias de refatoração para referências circulares são úteis não só no Go, mas também em outras linguagens

4 comentários

 
bus710 2025-04-25

No começo, quando você monta tudo correndo e faz funcionar, é divertido.
Mas, quando começa a colocar testes,
você acaba pensando por que fez daquele jeito naquela época.

 
bungker 2025-04-24

A frase "eu queria uma banana, mas trouxeram a floresta inteira" é muito engraçada.

 
iwanhae 2025-04-24

Acho que uma das coisas mais difíceis ao desenvolver com Spring era a dependência circular..
Aquela frustração de ficar inicializando um ao outro infinitamente e acabar travando por vazamento de memória...

 
GN⁺ 2025-04-24
Comentários no Hacker News
  • Não permitir dependências circulares é uma excelente escolha de projeto ao construir programas de grande porte

    • Isso força uma separação adequada de responsabilidades
    • Quando surge uma dependência circular, há um problema no projeto, e o artigo explica bem como resolvê-lo
    • Às vezes, dependências circulares são resolvidas usando ponteiros de função redefinidos por outro pacote
    • Seria bom se o compilador de Go fornecesse uma saída mais útil ao criar dependências circulares
    • Atualmente, ele fornece a lista de todos os pacotes envolvidos no loop, o que pode ser bem longo, e geralmente o causador do problema foi o último que você alterou
  • Excelente post de blog

    • Há muitos posts incríveis nesse site, e, se você gosta de aprender sobre programação funcional, recomendo dar uma olhada
    • Link
  • Uma técnica bônus relacionada ao conselho de "mover para um terceiro pacote"

    • Se você gerar muitas estruturas de modelo (SQL, Protobuf, GraphQL etc.), pode estabelecer uma direcionalidade clara entre as camadas geradas
    • Todo o código gerado é fornecido ao código da aplicação como um "pacote base", compondo tudo junto
    • Antes de adotar essa técnica, havia um problema em que "modelos importavam modelos de forma circular", mas isso desapareceu completamente com a introdução de uma camada estrutural adicional
  • Parece que está lendo um livro sobre o método estruturado de Yourdon

  • Pacotes não podem fazer referência circular entre si

    • Na verdade, em Go isso é possível usando go:linkname
  • Lembra o conceito concreto de randomizer

  • Uma característica curiosa de Golang é que você não pode ter dependências circulares no nível de pacote, mas pode tê-las no go.mod

    • Resumindo, você também não deveria fazer isso
  • Uma ótima explicação de como Jerf pensa sobre pacotes e lida com dependências circulares