Composición mediante Embedding en Go

En Go, el embedding es un mecanismo que permite incluir un tipo dentro de otro para que sus campos y métodos estén disponibles directamente en el tipo externo. No es herencia en el sentido tradicional de la Programación Orientada a Objetos (POO); es una forma de composición que “promueve” los miembros del tipo embebido al nivel del tipo contenedor.

Para entenderlo, imagina que un struct Logger incluye un *log.Logger. El compilador no crea una jerarquía de clases, sino que simplemente hace que las capacidades de log.Logger sean accesibles como si fueran parte de Logger. Si ejecutas l.Printf(...) en lugar de l.Logger.Printf(...), no es porque l sea un logger, sino porque el método Printf ha sido promovido.

Esta decisión de diseño evita el problema de las jerarquías de clases profundas y rígidas. En lugar de decir que un objeto “es un” (is-a) tipo, decimos que un objeto “tiene un” (has-a) tipo, pero lo hacemos de una manera que permite delegar comportamientos de forma transparente. Debes usarlo cuando quieras construir tipos más complejos reutilizando lógica existente, como añadir metadatos a un objeto o envolver un tipo para interceptar sus llamadas. Si intentas usarlo para forzar polimorfismo (pasar un tipo embebido a una función que espera al tipo base), el código no compilará.

───

package main

import (
	"fmt"
)

// Base representa un recurso con comportamiento básico.
type Base struct {
	ID   int
	Name string
}

func (b Base) Info() string {
	return fmt.Sprintf("Base ID: %d, Nombre: %s", b.ID, b.Name)
}

func (b Base) Greet() {
	fmt.Printf("Hola desde la base (%s)\n", b.Name)
}

// Audit es un tipo que usa embedding para extender a Base.
type Audit struct {
	Base  // Embedding de Base
	User string
}

// Shadowing: Redefinimos Info para "sombrear" el método de Base.
// No es un override; es una nueva definición que oculta a la anterior.
func (a Audit) Info() string {
	return fmt.Sprintf("[AUDIT] Usuario: %s | %s", a.User, a.Base.Info())
}

// Tag es otro tipo que también embebe a Base.
type Tag struct {
	Base
	Tag string
}

// Hybrid combina dos tipos que comparten el mismo tipo embebido.
type Hybrid struct {
	Audit
	Tag
}

func main() {
	// 1. Promoción y Sombreado (Shadowing)
	// El campo Name y el método Greet se promueven desde Base.
	aud := Audit{
		Base: Base{ID: 1, Name: "Servidor-Prod"},
		User: "admin",
	}

	fmt.Println("--- Caso: Audit (Shadowing) ---")
	aud.Greet() // Acceso directo por promoción de Base.Greet
	fmt.Println(aud.Info()) // Usa la versión de Audit, no la de Base
	fmt.Println("Nombre:", aud.Name) // Acceso directo por promoción de Base.Name

	// 2. Ambigüedad en Multiple Embedding
	// Hybrid contiene Audit y Tag. Ambos tienen el método Greet de Base.
	h := Hybrid{
		Audit: Audit{
			Base: Base{ID: 10, Name: "Híbrido-A"},
			User: "dev",
		},
		Tag: Tag{
			Base: Base{ID: 20, Name: "Híbrido-B"},
			Tag: "Etiqueta-X",
		},
	}

	fmt.Println("\n--- Caso: Hybrid (Ambigüedad) ---")
	
	// h.Greet() 
	// ^ ERROR: "ambiguous selector v.Greet" 
	// El compilador no sabe si quieres la Greet de Audit o la de Tag.

	// Para resolver la ambigüedad, debemos ser explícitos:
	fmt.Println("Resolución manual (vía Audit):", h.Audit.Greet())
	fmt.Println("Resolución manual (vía Tag):", h.Tag.Greet())

	// 3. Acceso a campos promovidos no conflictivos
	// Tag.Tag y Audit.User son únicos en el nivel superior.
	fmt.Printf("\nTag: %s, User: %s\n", h.Tag.Tag, h.User)
}

───

Desglose del ejemplo

  • Promoción de métodos y campos: En la variable aud, llamamos a aud.Greet(). Como Audit no tiene un método Greet, el compilador busca en sus tipos embebidos y encuentra Base.Greet(). Lo mismo ocurre con aud.Name, que accede directamente al campo de la estructura interna.
  • Sombreado (Shadowing): Al definir func (a Audit) Info(), hemos creado un método con la misma firma que el de Base. A partir de ese momento, para cualquier instancia de Audit, Info() se refiere a la versión de Audit. Sin embargo, dentro de Audit.Info(), todavía podemos acceder a la versión original mediante a.Base.Info().
  • Ambigüedad: El struct Hybrid es el ejemplo crítico. Aunque Hybrid no tiene un método Greet, sus componentes Audit y Tag sí lo tienen (vía Base). Cuando intentas llamar a h.Greet(), el compilador se detiene porque la selección es ambigua. La solución es “bajar” un nivel en la jerarquía de composición para especificar la ruta exacta: h.Audit.Greet().

El error frecuente

El error más común para quienes vienen de Java o Python es intentar usar el embedding para lograr polimorfismo implícito. En Go, el tipo embebido no es un subtipo del tipo contenedor.

type Base struct{}
func (b Base) Do() {}

type Derived struct {
    Base
}

func Process(b Base) {
    b.Do()
}

func main() {
    d := Derived{}
    Process(d) // ¡ERROR DE COMPPILACIÓN!
    // cannot use d (type Derived) as type Base in argument to Process
}

Aunque Derived tiene acceso a Do(), Derived no es una Base. Si una función requiere una Base, debes pasarle explícitamente el campo embebido: Process(d.Base).

60

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio