27 puntos por GN⁺ 2025-04-24 | 4 comentarios | Compartir por WhatsApp
  • Como el lenguaje Go prohíbe estrictamente las referencias circulares entre paquetes, esto lleva de forma natural a un diseño por capas (layered design)
  • Este artículo explica la estructura en capas que los proyectos Go terminan teniendo por obligación y sostiene que ya es suficientemente válida, incluso sin imponer una arquitectura adicional encima
  • Cuando aparecen dependencias circulares, presenta estrategias de refactorización concretas y prácticas para resolverlas paso a paso
  • Cada paquete se diseña para tener una unidad funcional con significado propio, lo que también favorece las pruebas, el mantenimiento y la separación en microservicios
  • En consecuencia, este enfoque evita el problema frecuente en el diseño de código de "querías un plátano y te trajeron toda la selva"

Enfoque de diseño por capas en Go

Principios básicos

  • Go prohíbe las referencias circulares entre paquetes
  • La relación de imports de todo programa Go debe formar un grafo acíclico dirigido (DAG)
  • Esta estructura no es una opción, sino una regla de diseño impuesta a nivel del lenguaje

Formación automática de capas de paquetes

  • Excluyendo los paquetes externos, los paquetes internos del proyecto pueden organizarse automáticamente en capas según la profundidad de sus referencias
  • Como en la figura de abajo, en la base se ubican paquetes utilitarios esenciales como metrics, logging y estructuras de datos compartidas
  • Después, los paquetes superiores van combinando funcionalidad y apilándose hacia arriba

Características de este enfoque de diseño

  • Las capas se basan en la dirección de las referencias, no en una abstracción jerárquica
  • Un paquete puede referenciar varios paquetes de niveles inferiores
  • Enfoques de diseño existentes como MVC o la arquitectura hexagonal también pueden "aplicarse" sobre esta estructura
    → Eso sí, siempre hay que considerar las restricciones estructurales de Go

Estrategias para resolver referencias circulares

Si aparece una referencia circular, intenta refactorizar en este orden:

1. Mover funcionalidad

  • La opción más recomendada
  • Analiza con precisión la funcionalidad que provoca el ciclo y muévela al lugar lógicamente adecuado
  • No se usa con mucha frecuencia, pero es la que más mejora la claridad conceptual

2. Separar la funcionalidad compartida en otro paquete

  • Mueve a un tercer paquete los tipos o funciones que ambos lados usan en común (Username, por ejemplo)
  • Aunque el paquete parezca pequeño, sepáralo sin miedo
    → Con el tiempo, es muy probable que ese paquete crezca

3. Crear un paquete superior de composición

  • Crea un tercer paquete que combine los dos paquetes que están en ciclo
  • Ejemplo: separar en un paquete superior la dependencia bidireccional entre Category y BlogPost
    → Los paquetes inferiores se mantienen como dumb struct, y la funcionalidad real se compone en el paquete superior

4. Introducir interfaces

  • Sustituye la dependencia por una interfaz que solo tenga los métodos que necesita la estructura o función
  • Esto elimina dependencias innecesarias y mejora la facilidad de prueba
  • Pero si se usa en exceso, el diseño puede volverse más complejo

5. Copiar (Copy)

  • Si aquello de lo que dependes es muy pequeño, simplemente cópialo y úsalo
  • Puede parecer una violación de DRY, pero en muchos casos en la práctica ayuda a aclarar el diseño

6. Unirlo en un solo paquete

  • Si ninguno de los métodos anteriores es posible, fusiona los dos paquetes
  • Es aceptable siempre que no se convierta en un paquete demasiado grande
    → Aun así, evita fusionar por inercia y tómalo con cuidado

Ventajas prácticas de este enfoque de diseño

  • Cada paquete tiene una unidad funcional significativa por sí misma y puede probarse de manera independiente
  • Como las referencias dentro del paquete están limitadas, es posible entender un paquete individual sin comprender todo el código
  • Evita conexiones de dependencias globales no intencionales (= el problema de la selva) y fomenta escribir código que use solo lo necesario
  • También es fácil de extraer al separar microservicios
    → La mayoría de las dependencias ya están definidas con claridad

Conclusión

  • Las restricciones de diseño de paquetes en Go no son una molestia, sino un mecanismo que guía hacia un buen diseño
  • Incluso sin una arquitectura especial, una estructura sólida puede lograrse solo con la forma en que se referencian los paquetes entre sí
  • El análisis cuidadoso y las estrategias de refactorización frente a referencias circulares son útiles no solo en Go, sino también en otros lenguajes

4 comentarios

 
bus710 2025-04-25

Es divertido cuando lo armas rápido y funciona,
pero cuando empiezas a meter tests,
te pones a pensar por qué lo hiciste así en ese momento.

 
bungker 2025-04-24

La frase "quería una banana, pero trajo toda la jungla" está buenísima.

 
iwanhae 2025-04-24

Creo que una de las cosas más difíciles al desarrollar con Spring era la dependencia circular..
Esa frustración de que se inicialicen mutuamente de forma infinita y terminen colapsando por una fuga de memoria...

 
GN⁺ 2025-04-24
Opiniones en Hacker News
  • No permitir dependencias circulares es una excelente decisión de diseño al construir programas grandes

    • Esto obliga a separar adecuadamente las responsabilidades
    • Si aparecen dependencias circulares, hay un problema en el diseño, y el artículo explica bien cómo resolverlo
    • A veces se resuelven las dependencias circulares usando punteros a función que otros paquetes redefinen
    • Ojalá el compilador de Go diera una salida más útil cuando se crean dependencias circulares
    • Actualmente muestra una lista de todos los paquetes involucrados en el ciclo, que puede ser bastante larga, y por lo general el que causó el problema es el último que cambiaste
  • Es una excelente entrada de blog

    • Este sitio web tiene publicaciones sorprendentes, y si te gusta aprender sobre programación funcional, te recomiendo revisarlo
    • Enlace
  • Una técnica extra relacionada con el consejo de "moverlo a un tercer paquete"

    • Si generas muchas estructuras de modelos (SQL, Protobuf, GraphQL, etc.), puedes establecer una direccionalidad clara entre las capas generadas
    • Todo el código generado se entrega al código de la aplicación como un "paquete base" para componerlo todo junto
    • Antes de introducir esta técnica, existía el problema de que "los modelos importaban otros modelos de forma circular", pero desapareció por completo al añadir una capa estructural adicional
  • Parece que está leyendo un libro sobre el método estructurado de Yourdon

  • Los paquetes no pueden referenciarse circularmente entre sí

    • En realidad, en Go eso es posible usando go:linkname
  • Me recuerda al concepto concreto de randomizer

  • Una característica curiosa de Golang es que no puede tener dependencias circulares a nivel de paquetes, pero sí en go.mod

    • En resumen, tampoco deberías hacer eso
  • Es una buena explicación de cómo Jerf piensa sobre los paquetes y cómo maneja las dependencias circulares