Struct Embedding: Composición vs Herencia Hecha Bien (Y La Mayoría Lo Hace Mal)
Una exploración profunda de struct embedding en Go: cómo Go permite composición a través de embedding, por qué parece herencia pero no lo es, qué problemas causa cuando se usa mal, y cómo realmente deberías usarlo. Con 40+ ejemplos de código mostrando anti-patrones y el enfoque correcto.
Go no tiene herencia. Lo saben todos.
Pero Go tiene struct embedding, y aquí es donde la mayoría de los equipos comete su primer gran error arquitectónico.
Ven esto:
type Animal struct {
Name string
}
func (a *Animal) Speak() {
fmt.Println(a.Name, "makes a sound")
}
type Dog struct {
Animal // Embedding
Breed string
}
dog := &Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
dog.Speak() // Funciona. Parece herencia.
fmt.Println(dog.Name) // Acceso "directo". Parece herencia.
Y piensan: “Ah, esto es herencia en Go.”
No lo es.
Es peor. Es herencia disfrazada de composición, lo que combina los peores aspectos de ambas.
Los equipos que usan struct embedding sin entender realmente qué está sucediendo terminan con arquitecturas que:
- Parecen flexibles pero son rígidas
- Parecen simples pero son complejas
- Parecen composición pero son herencia
- Violan el principio de responsabilidad única en formas sutiles
Este artículo es una deconstrucción completa de struct embedding: qué es realmente, qué problemas causa, y cómo usarlo correctamente (que es diferente a “como todos lo usan”).
Parte 1: Qué Es Struct Embedding, Realmente
1.1 La Definición Oficial (Que Es Confusa)
La documentación de Go define embedding así:
“Un struct puede contener otro struct. Cuando un struct anónimo está embebido, los nombres de sus campos y métodos aparecen automáticamente en el struct externo.”
Esto suena inocente. De hecho suena útil.
En realidad, esta característica es el origen de un montón de confusiones arquitectónicas.
1.2 Lo Que Realmente Sucede
Cuando escribes:
type Animal struct {
Name string
}
type Dog struct {
Animal
Breed string
}
Go está haciendo name promotion de los campos de Animal al struct Dog.
// Esto:
type Dog struct {
Animal
}
// Es equivalente a (casi):
type Dog struct {
Animal Animal // Campo nombrado explícitamente
}
// Pero con name promotion para campos y métodos:
dog := &Dog{}
dog.Name = "Rex" // Promovido desde Animal.Name
dog.Speak() // Promovido desde Animal.Speak()
// VS sin embedding:
dog := &Dog{}
dog.Animal.Name = "Rex" // Acceso explícito
dog.Animal.Speak() // Llamada explícita
El name promotion hace que parezca como si el field Name perteneciera directamente a Dog. Pero realmente está dentro de Animal.
1.3 Por Qué Parece Herencia
La razón por la que struct embedding se parece a herencia:
- Acceso directo de fields:
dog.Nameen lugar dedog.Animal.Name - Acceso directo de métodos:
dog.Speak()en lugar dedog.Animal.Speak() - Parece “es un” en lugar de “tiene un”: Leyendo el código, parece que
DogIS Animal
Todos esos cambios están diseñados para hacer el código más conciso.
Pero crean la ilusión de herencia.
1.4 Por Qué No Es Herencia
Struct embedding no es herencia porque:
- No hay polymorfismo: No puedes pasar
Dogdonde se esperaAnimal - No hay method override: No puedes sobrescribir
Speak()de forma polymórfica - No hay constructor chaining: No hay
super()oinit()implícito - Es composición literal: Tienes un field
Animaldentro deDog
Pero se comporta tan similar a herencia que todos la tratan como tal.
Parte 2: Por Qué La Mayoría Lo Usa Mal
2.1 Anti-Patrón #1: Usar Embedding Como Herencia
Este es el error más común. Escribes:
// ❌ Treating embedding like inheritance
type Entity struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
Entity // ← "Herencia" de Entity
Email string
Name string
}
type Product struct {
Entity // ← "Herencia" de Entity
SKU string
Price float64
}
// Parece que User y Product "heredan" de Entity
// Pero lo que realmente pasa es que ambos CONTIENEN un Entity embebido
Esto parece elegante. Un struct base con campos comunes, reutilizados por múltiples tipos.
Pero crea problemas:
Problema 1: Campos ambiguos
user := &User{
Entity: Entity{
ID: "123",
CreatedAt: time.Now(),
},
Email: "john@example.com",
Name: "John",
}
// ¿user.ID es un campo de User o de Entity?
// Pregunta equivocada. Es un campo de Entity promovido a User.
// Pero el código hace parecer que pertenece a User.
// Cuando necesitas refactorizar, esto se vuelve confuso.
// "¿Dónde se asigna user.ID?"
Problema 2: Semántica confusa
// Semánticamente, parece que estás diciendo:
// "User ES un Entity"
type User struct {
Entity
}
// Pero realmente quieres decir:
// "User TIENE-UN Entity que contiene metadata"
type User struct {
Metadata Entity // Más claro
}
// La semántica importa. Los lectores asumen herencia.
Problema 3: Métodos promovidos inesperados
// Si Entity tiene métodos que User hereda
type Entity struct {
ID string
}
func (e *Entity) Delete(ctx context.Context) error {
return deleteFromDB(ctx, e.ID)
}
type User struct {
Entity
Email string
}
// Ahora User tiene automáticamente un método Delete
user.Delete(ctx) // Pero ¿debería User tener Delete?
// ¿O solo Entity debería tenerlo?
// Si User necesita una semántica diferente de Delete, ahora tienes un problema.
2.2 Anti-Patrón #2: Embedding Múltiple Niveles
// ❌ Múltiples niveles de embedding
type Timestamp struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type Entity struct {
Timestamp // Embebido
ID string
}
type User struct {
Entity // Embebido
Email string
}
// Ahora User tiene:
// - User.Email (directo)
// - User.ID (promovido de Entity)
// - User.CreatedAt (promovido de Entity → promovido de Timestamp)
// - User.UpdatedAt (promovido de Entity → promovido de Timestamp)
user := &User{}
user.CreatedAt = time.Now() // ¿De dónde viene esto?
// User → Entity → Timestamp
// Pero el código no lo muestra
Multi-level embedding crea name promotion indirecto. Es confuso.
2.3 Anti-Patrón #3: Ambiguedad en la Resolución de Nombres
// ❌ Conflictos potenciales
type Logger struct {
Level string
}
type Config struct {
Level string
}
type Service struct {
Logger // Embebido, tiene Level
Config // Embebido, tiene Level
Name string
}
// ¿service.Level de quién es?
service := &Service{}
service.Level = "DEBUG" // Ambiguo. Go reporta un error.
// "ambiguous selector service.Level"
// Debes ser explícito:
service.Logger.Level = "DEBUG"
service.Config.Level = "INFO"
Go prohíbe la ambiguedad explícita. Pero la ambiguedad CONCEPTUAL sigue ahí.
2.4 Anti-Patrón #4: Violación de Responsabilidad Única
// ❌ Demasiadas responsabilidades
type Persistable struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
func (p *Persistable) Save(ctx context.Context) error { ... }
func (p *Persistable) Delete(ctx context.Context) error { ... }
}
type User struct {
Persistable // Usuario hereda persistencia
Email string
}
// Ahora User tiene responsabilidades de:
// 1. Ser un Usuario (Email, Name, etc)
// 2. Ser Persistible (Save, Delete)
// Pero ¿debería User realmente saber cómo persistirse?
// ¿O debería un Repository saber cómo persistir Users?
// El embedding sugiere que User debería saber cómo persistirse.
// Eso es arquitectura de 2000, no moderna.
2.5 Anti-Patrón #5: Inflexibilidad Disfrazada de Flexibilidad
// ❌ Inflexible pero se parece flexible
type BaseHandler struct {
Logger Logger
Config Config
}
type UserHandler struct {
BaseHandler
}
type ProductHandler struct {
BaseHandler
}
// Ahora ambos handlers heredan Logger y Config
// Parece flexible: puedes compartir comportamiento
// Pero la realidad:
// - Si UserHandler necesita un logger diferente, está atrapado
// - Si ProductHandler necesita una configuración diferente, está atrapado
// - Si quieres agregar un campo a BaseHandler, afecta a todos
// - Si quieres remover un campo, refactorización pesada
// Es la clásica herencia de clase que hace arquitectura inflexible.
Parte 3: Por Qué Los Equipos Se Equivocan
3.1 Razón #1: Parece Herencia, Huele a Herencia
Los desarrolladores vienen de lenguajes con herencia:
// Java
class Dog extends Animal {
String breed;
}
// Python
class Dog(Animal):
def __init__(self):
self.breed = ""
// C++
class Dog : public Animal {
string breed;
};
Cuando ven struct embedding en Go, asumen que es lo mismo:
// Go - pero los desarrolladores ven herencia
type Dog struct {
Animal
Breed string
}
Es una asunción natural. Y es un error.
3.2 Razón #2: La Documentación De Go No Deja Claro
Si lees la documentación de Go sobre embedding, dice:
“When an anonymous field is accessed, its type name serves as a field name.”
Eso es técnicamente preciso. Pero no explica que:
- Esto NO es herencia
- Esto NO es polimorfismo
- Esto ESTÁ creando confusión semántica
- Esto DEBERÍA usarse raramente
3.3 Razón #3: Te Permite Código Más Corto
Struct embedding permite escribir código más conciso:
// Con embedding
dog.Name = "Rex"
// Sin embedding
dog.Animal.Name = "Rex"
Los equipos aman código conciso. Así que generalizamos embedding para todas las situaciones.
3.4 Razón #4: Falta de Alternativas Claras
Si no sabes cómo compartir comportamiento sin embedding, ¿qué haces?
// ❌ Embedding (parece incorrecto pero funciona)
type User struct {
Entity
Email string
}
// ❌ Explícito (verboso)
type User struct {
Entity Entity // ← Parece redundante
Email string
}
// ✅ Composición real (pero requiere refactorizar)
type User struct {
ID string
Email string
}
func (u *User) GetMetadata() *Metadata { ... }
// La mayoría elige embedding porque es "lo intermedio"
Parte 4: Cuándo Realmente Debería Usar Embedding
4.1 Caso De Uso #1: Extensión de Tipos Simples
El caso legítimo más común:
// ✅ Embedding para extensión
type Duration time.Duration
func (d Duration) String() string {
return fmt.Sprintf("%.2f seconds", d.Seconds())
}
type Timeout struct {
Duration
MaxRetries int
}
timeout := &Timeout{
Duration: Duration(5 * time.Second),
MaxRetries: 3,
}
fmt.Println(timeout.String()) // Reutiliza String() de Duration
Aquí, embedding es apropiado porque:
- Estás realmente extendiendo un tipo
- El tipo embebido es simple y bien definido
- Los métodos promovidos tienen sentido semántico
- Estás dentro de un dominio pequeño (no herencia profunda)
4.2 Caso De Uso #2: Mixins Para Comportamiento Reutilizable
Cuando quieres “mezclar” comportamiento pequeño:
// ✅ Mixin pattern
type Timestamped struct {
CreatedAt time.Time
UpdatedAt time.Time
}
func (t *Timestamped) Touch() {
t.UpdatedAt = time.Now()
}
type Document struct {
Timestamped // Mixin
Title string
Content string
}
type Comment struct {
Timestamped // Mismo mixin
Text string
AuthorID string
}
// Ambos tipos obtienen Touch()
doc := &Document{Title: "Hello"}
doc.Touch()
comment := &Comment{Text: "Great article"}
comment.Touch()
Pero incluso aquí, es cuestionable si debería ser embedding o composición explícita.
4.3 Caso De Uso #3: Testing - Fakes Y Stubs
El caso donde embedding tiene más sentido:
// ✅ Testing con embedding
type Repository interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
type FakeRepository struct {
GetUserFunc func(ctx context.Context, id string) (*User, error)
SaveUserFunc func(ctx context.Context, user *User) error
}
func (f *FakeRepository) GetUser(ctx context.Context, id string) (*User, error) {
return f.GetUserFunc(ctx, id)
}
func (f *FakeRepository) SaveUser(ctx context.Context, user *User) error {
return f.SaveUserFunc(ctx, user)
}
// En testing
repo := &FakeRepository{
GetUserFunc: func(ctx context.Context, id string) (*User, error) {
return &User{ID: id, Email: "test@example.com"}, nil
},
}
// Aquí podrías usar embedding para reducir boilerplate:
type FakeRepositoryWithEmbedding struct {
Repository // Embebir la interfaz real
// Pero Go no permite esto exactamente
}
En realidad, Go no permite incrustar interfaces de manera que permita override de métodos, así que esto no funciona bien.
4.4 Cuándo NO Deberías Usar Embedding
Regla #1: Si necesitas comportamiento polimórfico, NO uses embedding
// ❌ INCORRECTO
type Shape struct {
// embedding no permite override polimórfico
}
func (s *Shape) Area() float64 {
return 0
}
type Circle struct {
Shape // ← Embebido
Radius float64
}
func (c *Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Circle.Area() "overrides" Shape.Area()
// Pero esto es shadowing, no polimorfismo
// No puedes pasar Circle donde se espera Shape
// ✅ CORRECTO: Usa interfaz
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c *Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
Regla #2: Si necesitas flexibilidad de configuración, NO uses embedding
// ❌ INCORRECTO: Inflexible
type BaseService struct {
Logger Logger
Config Config
}
type UserService struct {
BaseService
}
// Si UserService necesita un Logger diferente:
// 1. No puedes cambiar el BaseService.Logger después
// 2. Si necesitas inyectar un logger diferente en tests, es difícil
// 3. Si necesitas un logger condicional, es imposible
// ✅ CORRECTO: Composición explícita
type UserService struct {
Logger Logger
Config Config
}
// Ahora es claro qué tienes
// Puedes cambiar Logger
// Puedes mockear Logger
// Puedes inyectar diferentes loggers
Regla #3: Si hay campos con el mismo nombre, NO uses embedding
// ❌ INCORRECTO
type Metadata struct {
Version string
}
type Config struct {
Version string
}
type Service struct {
Metadata // Conflicto potencial
Config // Conflicto potencial
}
// Go reclama ambiguedad explícita
// Pero si necesitas ambos, es una señal de que no deberían estar embebidos
// ✅ CORRECTO
type Service struct {
Metadata Metadata
Config Config
}
service.Metadata.Version = "1.0"
service.Config.Version = "2.0" // Claro quién es quién
Parte 5: El Enfoque Correcto
5.1 El Patrón: Composición Explícita
En lugar de:
// ❌ Confuso
type User struct {
Entity // ← Ambiguo: ¿de dónde vienen los campos?
Email string
}
Hazlo:
// ✅ Claro
type User struct {
Metadata *Metadata // Campo nombrado explícitamente
Email string
}
// O si la metadata es inline:
type User struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
Email string
}
// Luego si necesitas reutilizar campos en múltiples tipos:
// Crea un struct específico
type EntityMetadata struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
EntityMetadata
Email string
}
// PERO: Solo si realmente necesitas reutilizar EXACTAMENTE
// Y solo si hace el código más claro, no más confuso
5.2 El Patrón: Interfaz en Lugar de Embedding
// ❌ Embedding para comportamiento común
type CommonHandler struct {
Logger Logger
Config Config
}
type UserHandler struct {
CommonHandler
}
type ProductHandler struct {
CommonHandler
}
// ✅ Interfaz para contrato
type Handler interface {
GetLogger() Logger
GetConfig() Config
Handle(ctx context.Context) error
}
type UserHandler struct {
logger Logger
config Config
}
func (h *UserHandler) GetLogger() Logger { return h.logger }
func (h *UserHandler) GetConfig() Config { return h.config }
func (h *UserHandler) Handle(ctx context.Context) error { ... }
type ProductHandler struct {
logger Logger
config Config
}
func (h *ProductHandler) GetLogger() Logger { return h.logger }
func (h *ProductHandler) GetConfig() Config { return h.config }
func (h *ProductHandler) Handle(ctx context.Context) error { ... }
// Ahora cada handler es independiente
// Puedes inyectar diferentes loggers
// Puedes mockarlos fácilmente
// Es extensible sin afectar otros handlers
5.3 El Patrón: Wrap en Lugar de Embed
// ❌ Embedding para métodos útiles
type BaseValidator struct {
Errors []string
}
func (b *BaseValidator) AddError(e string) {
b.Errors = append(b.Errors, e)
}
type UserValidator struct {
BaseValidator
User *User
}
func (v *UserValidator) Validate() error {
if v.User.Email == "" {
v.AddError("Email is required")
}
if len(v.Errors) > 0 {
return errors.New(strings.Join(v.Errors, "; "))
}
return nil
}
// ✅ Wrap explícitamente
type ValidatorState struct {
Errors []string
}
func (v *ValidatorState) AddError(e string) {
v.Errors = append(v.Errors, e)
}
type UserValidator struct {
state *ValidatorState
user *User
}
func (v *UserValidator) Validate() error {
if v.user.Email == "" {
v.state.AddError("Email is required")
}
if len(v.state.Errors) > 0 {
return errors.New(strings.Join(v.state.Errors, "; "))
}
return nil
}
// Es más código, pero es completamente claro
// Qué responsabilidad tiene qué
// Cómo fluyen los datos
// Qué depende de qué
5.4 Decision Tree: ¿Debo Usar Embedding?
┌─ ¿Necesito reutilizar campos?
│
├─ SÍ ──────────────────────────────────────────┐
│ │
│ ├─ ¿Hay 3+ tipos que reutilizan EXACTAMENTE |
│ │ los mismos campos? |
│ │ │
│ │ ├─ SÍ → Crea struct específico y compón │
│ │ │ explícitamente │
│ │ │ (Metadata struct) │
│ │ │ │
│ │ └─ NO → Duplica los campos. Sí, en serio│
│ │ Es más claro que embedding │
│ │ │
│ └─ ¿Los métodos promovidos tienen │
│ sentido semántico para el tipo que │
│ embebe? │
│ │
│ ├─ SÍ → Posiblemente OK (raros casos) │
│ └─ NO → Usa composición explícita │
│ │
└─ NO → No uses embedding (mejor alternativa)
Conclusión: En >95% de casos, usa composición explícita.
Parte 6: Ejemplos Reales del Problema
6.1 Caso: Startup de 5 Personas
El Plan: “Usar embedding para reutilizar metadata en todos los modelos”
El Código:
type Entity struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
Entity
Email string
Name string
}
type Order struct {
Entity
UserID string
Total float64
}
type Product struct {
Entity
SKU string
Price float64
}
Lo Que Sucedió:
Mes 1: "Perfecto, reutilizamos Entity"
Mes 3: "User necesita DeletedAt para soft deletes"
Opciones:
1. Agregamos DeletedAt a Entity (afecta Order y Product)
2. Creamos EntityWithDelete struct
Mes 6: "Product necesita Version para versionado"
Nuevamente, dilema: ¿agregar a Entity? ¿nuevo struct?
Mes 9: "Order tiene su propio status con timestamps, no usa Entity.UpdatedAt"
Ahora Order.UpdatedAt es confuso: ¿cuándo se actualiza?
Mes 12: Refactor:
- Remover embedding
- Crear structs de metadata específicos
- Actualizar 20+ funciones
- Costo: 2 semanas
Lección: “Si no todos los tipos necesitan exactamente lo mismo, embedding causa inflexibilidad, no flexibilidad.”
6.2 Caso: Empresa Mediana con ORM
El Plan: “Usar embedding con gorm para automatizar ID y timestamps”
El Código:
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
type User struct {
Model
Email string
Name string
}
type Order struct {
Model
UserID uint
Total float64
}
Lo Que Sucedió:
Mes 1-3: "Funciona perfecto"
Mes 4: "Necesitamos DeletedAt para soft deletes"
Opción A: Usar gorm.DeletedAt
type Model struct {
DeletedAt gorm.DeletedAt
}
Ahora todos los tipos tienen soft delete, incluso si no lo necesitan.
Opción B: Crear separado
type SoftDeleteModel struct {
Model
DeletedAt gorm.DeletedAt
}
Ahora tienes dos tipos base que mantener.
Mes 5: "Las transacciones no funcionan correctamente"
Resulta que Model.ID causa conflictos con transacciones
de gorm. Embedding ID es confuso para gorm.
Mes 6: Refactor completo a:
type User struct {
ID uint
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
Sin embedding. Más código, pero más claro.
Lección: “Embedding con ORMs causa más problemas de los que resuelve. Los ORMs manejan mejor composición explícita.”
6.3 Caso: Arquitectura Hexagonal Mal Interpretada
El Plan: “Usar embedding para reutilizar repositories”
El Código:
type BaseRepository struct {
db *sql.DB
logger Logger
}
type UserRepository struct {
BaseRepository
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*User, error) {
// Usa r.db
// Usa r.logger
}
type OrderRepository struct {
BaseRepository
}
func (r *OrderRepository) GetByID(ctx context.Context, id string) (*Order, error) {
// Usa r.db
// Usa r.logger
}
Lo Que Sucedió:
Mes 1: "Reutilizamos db y logger"
Mes 2: "Tests fallan porque no podemos mockar db en UserRepository"
Intentamos:
type FakeUserRepository struct {
BaseRepository
GetByIDFunc func(...) (*User, error)
}
Pero esto no funciona bien porque BaseRepository.db
sigue siendo la instancia real.
Mes 3: "Necesitamos inyectar diferentes loggers en tests"
Pero Logger está en BaseRepository.
No podemos cambiar solo para UserRepository.
Mes 4: Refactor a:
type UserRepository struct {
db *sql.DB
logger Logger
}
type OrderRepository struct {
db *sql.DB
logger Logger
}
Más código, pero tests son más simples.
Lección: “Embedding en arquitectura hexagonal causa problemas de testabilidad. Composición explícita es mejor.”
Parte 7: El Anti-Patrón Más Sutil
7.1 El Problema: Embedding Como “Type Aliasing”
// ❌ Subtle mistake
type Response struct {
http.Response // Embebido
CustomField string
}
// Parece que estás extendiendo http.Response
// Pero lo que realmente hiciste fue crear un struct que contiene uno
resp := &Response{
http.Response: http.Response{Status: "200"},
CustomField: "value",
}
// Ahora:
resp.Status // Acceso promovido a http.Response.Status
resp.StatusCode // Acceso promovido
resp.Header // Acceso promovido
// Pero si necesitas modificar cómo Response behaves:
// No puedes override los métodos de http.Response
// Solo los puedes shadowing
7.2 Por Qué Es Problemático
// Supongamos que necesitas un método customizado
func (r *Response) GetHeader(key string) string {
// Lógica custom
}
// Pero http.Response también tiene Header
// Ahora tienes:
r.Header // Acceso promovido a http.Response.Header
r.GetHeader("Content-Type") // Tu método custom
// ¿Cuál deberías usar?
// Confusión semántica.
// En herencia verdadera, sabrías que estás overriding.
// En embedding, es ambiguo.
Parte 8: Las Mejores Prácticas
8.1 Regla #1: Nombre Explícito de Composición
// ❌ INCORRECTO
type User struct {
Entity // ← ¿Qué es Entity exactamente?
Email string
}
// ✅ CORRECTO
type User struct {
Metadata *EntityMetadata // Claro qué es
Email string
}
// O si es inline:
type User struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
Email string
}
8.2 Regla #2: Evitar Multi-Level Embedding
// ❌ INCORRECTO
type A struct { X string }
type B struct { A }
type C struct { B }
c := &C{}
c.X = "value" // ¿De dónde viene X? C → B → A
// ✅ CORRECTO
type A struct { X string }
type B struct { A A } // Nombrado explícitamente
type C struct { B B } // Nombrado explícitamente
c := &C{}
c.B.A.X = "value" // Claro el camino
8.3 Regla #3: Si Necesitas Herencia, Usa Interfaz
// ❌ INCORRECTO: Intentar usar embedding como herencia
type Shape struct {
color string
}
type Circle struct {
Shape
radius float64
}
func (c *Circle) Area() float64 { ... }
// ✅ CORRECTO: Usar interfaz
type Shape interface {
Area() float64
}
type Circle struct {
color string
radius float64
}
func (c *Circle) Area() float64 { ... }
8.4 Regla #4: Composición Explícita Para Inyección
// ❌ INCORRECTO: Difícil de testear
type Service struct {
logger Logger
// logger está embebido, difícil de mockar
}
type UserService struct {
Service
}
// ✅ CORRECTO: Composición explícita
type UserService struct {
logger Logger
}
// Fácil de inyectar en tests:
userService := &UserService{
logger: &fakeLogger{},
}
8.5 Regla #5: Evitar Embedding de Interfaces
// ❌ INCORRECTO: Embedding interfaz
type Service struct {
Logger // Logger es una interfaz
}
type UserService struct {
Service
}
// Parece flexible, pero crea confusión:
us := &UserService{}
us.Infof("message") // ¿UserService es un logger?
// No, pero lo parece
// ✅ CORRECTO: Composición explícita de interfaz
type UserService struct {
logger Logger
}
func (us *UserService) Infof(format string, args ...interface{}) {
us.logger.Infof(format, args...)
}
// O si necesitas que UserService actúe como Logger:
func (us *UserService) Infof(format string, args ...interface{}) {
// Implementa el método
}
Parte 9: Cuándo Refactorizar
9.1 Señales De Que Tu Embedding Es Un Problema
Señal #1: Necesitas acceso a embedded type explícitamente
// Si escribes esto:
type User struct {
Entity
}
user := &User{}
user.Entity.ID = "123" // ← Acceso explícito
// Entonces probablemente debería ser:
type User struct {
Entity Entity // Nombrado
}
user.Entity.ID = "123" // Ahora es claro que Entity es un field
Señal #2: No todos los métodos promovidos tienen sentido
type User struct {
Entity // Embebido
}
// Si Entity.Delete() no tiene sentido para User
// Entonces Entity está wrongly embebido
// ✅ Cambia a:
type User struct {
Metadata *Metadata // No incluyas Delete() innecesariamente
}
Señal #3: Tests se vuelven complicados
// Si necesitas esto en tests:
type FakeUser struct {
User
FakeRepository FakeRepository // No puedes testear bien
}
// Entonces:
type User struct {
Repository Repository
}
// Es mejor
type User struct {
repository Repository
}
9.2 Cómo Refactorizar
Paso 1: Identificar embebidos
// Antes
type User struct {
Entity
Email string
}
Paso 2: Hacerlos explícitos
// Después
type User struct {
Metadata *Metadata
Email string
}
Paso 3: Actualizar accesos
// Antes
user.ID = "123" // Promovido
// Después
user.Metadata.ID = "123" // Explícito
Paso 4: Actualizar tests
// Antes
user := &User{
Entity: Entity{ID: "123"},
}
// Después
user := &User{
Metadata: &Metadata{ID: "123"},
}
Parte 10: La Verdad Sobre Embedding
10.1 Cuándo Embedding Es Legítimo
1. Extender tipos simples (Duration, Time, custom primitivos): ✅
2. Mixins muy específicos y pequeños: Maybe
3. Testing con fakes: No, usa mocking
4. Compartir metadata entre múltiples tipos: No, usa explícito
5. Simular herencia: No, usa interfaces
6. Reutilizar comportamiento: No, usa composición
7. Reducir boilerplate: Costo > beneficio
Resumen: Embedding es útil en <5% de casos reales.
Usarlo en >95% de proyectos es un error.
10.2 El Patrón Go Idiomat
En Go idiomático:
// Evitas:
- Herencia (no existe)
- Embedding simulando herencia (anti-patrón)
- Multi-level embedding (confuso)
- Embedding interfaz (crea confusión)
// Usas:
- Interfaces para contrato
- Composición explícita para comportamiento
- Métodos pequeños bien nombrados
- Responsabilidades claras
10.3 La Pregunta Final
Antes de usar embedding, pregúntate:
1. "¿Realmente quiero que X sea 'un tipo de' Y?"
Si sí → Usa interfaz
Si no → No uses embedding
2. "¿Los métodos promovidos tienen sentido para el tipo externo?"
Si sí → Posiblemente embedding es OK (raro)
Si no → Usa composición explícita
3. "¿Esto hace el código más claro o más confuso?"
Si más claro → Quizás
Si más confuso → Usa composición explícita (siempre la respuesta)
Conclusión: La Verdad Incómoda
Struct embedding en Go es una característica de conveniencia que la mayoría de los equipos abusa.
Parece herencia, permite código conciso, y oculta complejidad bajo la apariencia de simplicidad.
Pero Go no tiene herencia por una razón: Porque herencia causa problemas arquitectónicos.
Struct embedding, cuando se usa para simular herencia (que es >90% de los casos), es herencia disfrazada. Combina los problemas de herencia con la confusión de no saber explícitamente que estás usándola.
La verdad:
- Struct embedding es una característica de conveniencia para casos muy específicos
- La mayoría de los equipos lo abusa
- La mayoría de los abusos causan problemas de arquitectura
- El costo de refactorizar después es alto
- La solución es usar composición explícita desde el inicio
Si tienes dudas sobre si deberías usar embedding, la respuesta es casi siempre: No.
Usa composición explícita. Será más código, pero será más claro, más mantenible, y más fácil de extender.
Y eso es lo que realmente importa en arquitectura de software.
Tags
Artículos relacionados
API Versioning Strategies: Cómo Evolucionar APIs sin Romper Clientes
Una guía exhaustiva sobre estrategias de versionado de APIs: URL versioning vs Header versioning, cómo deprecar endpoints sin shock, migration patterns reales, handling de cambios backwards-incompatibles, y decisiones arquitectónicas que importan. Con 50+ ejemplos de código en Go.
Automatizando tu vida con Go CLI: Guía profesional para crear herramientas de línea de comandos escalables
Una guía exhaustiva y paso a paso sobre cómo crear herramientas CLI escalables con Go 1.25.5: desde lo básico hasta proyectos empresariales complejos con flags, configuración, logging, y ejemplos prácticos para Windows y Linux.
La Capa de Repositorio en Go: Conexiones, Queries y Arquitectura Agnóstica
Una guía exhaustiva sobre cómo construir la capa de repositorio en Go: arquitectura hexagonal con ports, conexiones a MongoDB, PostgreSQL, SQLite, manejo de queries, validación, escalabilidad y cómo cambiar de base de datos sin tocar la lógica de negocio.