Idioms de Go: Composición sobre Herencia
El código Go se siente nativo cuando favorece la composición sobre los diseños con forma de herencia.
Busca en todas las páginas de la documentación
El código Go se siente nativo cuando favorece la composición sobre los diseños con forma de herencia.
Eso significa structs pequeñas, constructores explícitos, interfaces estrechas y envoltura de funciones estilo middleware en lugar de jerarquías de clases profundas copiadas de otros lenguajes.
Los lenguajes con herencia de clases fomentan el modelado "es-un": un CachedUserService es un UserService es un BaseService.
Go omite deliberadamente ese mecanismo.
Un struct contiene datos; los métodos adjuntan comportamiento con receptores.
Cuando dos tipos necesitan capacidades similares, o incrustas (embed) otro tipo para promover sus métodos, o programas a una interfaz compartida definida en el consumidor.
La incrustación (embedding) se parece a la herencia pero se comporta como delegación.
Un struct externo con un campo anónimo http.Handler obtiene ServeHTTP promovido a sí mismo, pero el tipo externo no es sustituible por el tipo interno en todos los contextos.
Las interfaces se satisfacen implícitamente: si tu tipo tiene los métodos, implementa la interfaz sin declaración.
Eso habilita el idiom aceptar interfaces, devolver structs: los llamadores dependen del comportamiento más pequeño que necesitan; tu paquete devuelve tipos concretos para que los llamadores no queden atados a las formas de tus interfaces.
Las APIs nativas de Go también favorecen las funciones constructoras (NewServer, OpenDB) que devuelven valores inicializados con campos no exportados, más opciones funcionales opcionales para configuración sin firmas New(a, b, c, ...) telescópicas.
La arquitectura de Go del día a día apila primitivas de composición en capas predecibles:
Solicitud HTTP
│
▼
middleware (func que envuelve func)
│
▼
handler (struct concreto, depende de interfaz)
│
▼
service (reglas de negocio, acepta interfaz Repository)
│
▼
repository (implementación postgres / mock)El middleware es composición a través de funciones: func(http.Handler) http.Handler envuelve comportamiento sin subclase.
Cada capa añade logging, autenticación o métricas, y luego llama al manejador interno.
Los límites de servicio y repositorio usan interfaces definidas por el consumidor (type UserStore interface { Get(ctx, id) (User, error) }) para que las pruebas puedan intercambiar simulaciones (fakes) sin generación de código.
Las opciones funcionales componen la configuración en el momento de la construcción: NewClient(WithTimeout(2*time.Second), WithRetries(3)) construye un cliente válido sin campos mutables exportados.
sync.Once compone la inicialización segura bajo demanda (lazy initialization) en un paquete sin las trampas de ordenación de init().
| Patrón | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Interfaces pequeñas | Simulaciones (fakes) fáciles, pruebas estables | Muchos tipos nombrados | Servicios de dominio, bibliotecas |
| Incrustación (Embedding) | Reutilización rápida de valores predeterminados | API promovida oculta | Decoradores, envoltorios delgados |
| Opciones funcionales | Constructores extensibles | Ligera curva de aprendizaje | Clientes, servidores, SDKs |
| Middleware de funciones | Mínima ceremonia | Más difícil con APIs de múltiples métodos | Interceptores HTTP, gRPC |
| Singletons globales | Acceso conveniente | Dependencias ocultas, dolor en pruebas | Evitar; preferir inyección explícita |
Los routers de frameworks (chi, gin, echo) y los interceptores gRPC se componen a través de funciones y hooks con forma de interfaz.
Tus paquetes de dominio aún deben exponer interfaces de dominio, no tipos de router, para que el cambio de infraestructura no reescriba la lógica de negocio.
Los equipos maduros de Go codifican las reglas de composición en listas de verificación de revisión, no en folklore.
Prefiere funciones simples hasta que aparezca una segunda implementación; luego extrae una interfaz en el paquete consumidor.
Mantén las interfaces con uno o dos métodos cuando sea posible: io.Reader, io.Writer y http.Handler son la escala a imitar.
Evita la contaminación de interfaces: declarar interfaces junto a cada tipo concreto "para pruebas" a menudo produce contratos amplios e inestables.
Usa la incrustación (embedding) para delegación real (envolver http.ResponseWriter para capturar códigos de estado), no para simular jerarquías de herencia.
Las opciones funcionales pertenecen a constructores exportados con varios ajustes ortogonales.
No las uses para ayudantes internos con dos parámetros.
Las capas de repositorio y servicio son composición en el límite del paquete: los servicios orquestan; los repositorios aíslan los detalles de SQL/HTTP/SDK.
Los controladores se mantienen delgados.
Anti-patrones a tener en cuenta: paquetes "dios" (god packages) que mezclan HTTP, SQL y reglas de negocio; flujo de control basado en panic; init() que se conecta a bases de datos de producción; singletons que ocultan dependencias de las firmas.
Los genéricos de Go 1.18+ reducen los ayudantes de colecciones repetitivos pero no reemplazan la composición de interfaces en los puntos de integración del sistema.
Busca genéricos dentro de algoritmos; mantén las APIs externas con structs e interfaces idiomáticas.
sync.Once protege la inicialización única; la inyección de dependencias con parámetros explícitos sigue siendo el valor predeterminado para la testeabilidad.No hay herencia de clases ni sobrescritura de métodos en una jerarquía de tipos.
Go ofrece incrustación (embedding) para promoción de métodos y polimorfismo de interfaces para sustitución de comportamiento.
Incrusta cuando quieras métodos promovidos en un decorador delgado (por ejemplo, envolviendo http.Handler).
Envuelve en un campo nombrado cuando necesites ocultar o exponer selectivamente el comportamiento interno.
A menudo un método para límites de infraestructura (Store, Publisher, Clock).
Añade métodos solo cuando los llamadores realmente los necesiten juntos.
Defínelas donde se consumen (el paquete que llama al comportamiento).
Las implementaciones en otros paquetes las satisfacen implícitamente.
Son comunes en paquetes estilo stdlib (grpc, otel) pero opcionales.
Úsalas cuando los constructores de otro modo se expandirían a través de muchos parámetros.
Pasa interfaces o parámetros de función a los constructores.
Las pruebas suministran simulaciones (fakes) sin subclase de tipos de producción.
En Go, sí en la práctica: una función envuelve un http.Handler (o manejador gRPC) para añadir comportamiento transversal antes de delegar hacia adentro.
Cuando los servicios de negocio necesitan pruebas estables sin una base de datos, o cuando el almacenamiento puede cambiar (Postgres, caché, API remota).
El estado global mutable del paquete oculta dependencias, complica las pruebas paralelas y fomenta errores de orden de inicialización.
Prefiere la inyección explícita de constructores.
Los genéricos ayudan a los algoritmos compartidos sobre tipos comparables.
El polimorfismo en tiempo de ejecución y los puntos de integración externos todavía dependen de las interfaces.
Jerarquías estilo Java/C#, campos de struct exportados en tipos de biblioteca, interfaces amplias y el uso de panic para errores esperados son señales comunes.
Empieza con la composición más simple que resuelva el problema.
Busca opciones, repositorios o middleware cuando aparezca un segundo eje de cambio.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado GC Green Tea, go fix modernizers - verificar parche en build), chi (última - verificar en build), gin (última - verificar en build), echo (última - verificar en build), google.golang.org/grpc (última - verificar en build), sigs.k8s.io/controller-runtime (última - verificar en build), kubebuilder (última - verificar en build), tinygo (última - verificar targets de placa en build), wazero (última - verificar en build), y golangci-lint (última - verificar en build).
Revisado por Chris St. John·Última actualización: 19 jul 2026