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 aaud.Greet(). ComoAuditno tiene un métodoGreet, el compilador busca en sus tipos embebidos y encuentraBase.Greet(). Lo mismo ocurre conaud.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 deBase. A partir de ese momento, para cualquier instancia deAudit,Info()se refiere a la versión deAudit. Sin embargo, dentro deAudit.Info(), todavía podemos acceder a la versión original mediantea.Base.Info(). - Ambigüedad: El struct
Hybrides el ejemplo crítico. AunqueHybridno tiene un métodoGreet, sus componentesAudityTagsí lo tienen (víaBase). Cuando intentas llamar ah.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).
N° 60