Struct Embedding: Composición vs Herencia Hecha Bien (Y La Mayoría Lo Hace Mal)

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.

Por Omar Flores

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:

  1. Parecen flexibles pero son rígidas
  2. Parecen simples pero son complejas
  3. Parecen composición pero son herencia
  4. 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:

  1. Acceso directo de fields: dog.Name en lugar de dog.Animal.Name
  2. Acceso directo de métodos: dog.Speak() en lugar de dog.Animal.Speak()
  3. Parece “es un” en lugar de “tiene un”: Leyendo el código, parece que Dog IS 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:

  1. No hay polymorfismo: No puedes pasar Dog donde se espera Animal
  2. No hay method override: No puedes sobrescribir Speak() de forma polymórfica
  3. No hay constructor chaining: No hay super() o init() implícito
  4. Es composición literal: Tienes un field Animal dentro de Dog

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

#golang #composition #embedding #inheritance #design #anti-patterns