La composición en Go es la forma de reutilizar comportamiento incrustando unos tipos dentro de otros, sin crear jerarquías rígidas de clases.
Si vienes de Java, C# o C++, probablemente tu cerebro esté cableado para pensar en términos de Herencia. Buscas la palabra extends. Quieres crear una clase base Vehiculo y que Coche herede de ella para reutilizar el método Arrancar().
En Go, la herencia no existe. No hay extends.
En lugar de crear jerarquías rígidas (“es un”), Go permite construir tipos ensamblando piezas más pequeñas (“tiene un”). Para hacer cómoda esa composición ofrece el embedding o incrustación.
Embedding: campos anónimos
Ya vimos brevemente en el artículo de Structs que podíamos meter un struct dentro de otro. Pero el Embedding va un paso más allá.
Cuando declaras un campo dentro de un struct sin darle un nombre explícito (poniendo solo el tipo), Go realiza una Promoción de Campos y Métodos.
Ejemplo práctico
Imagina un sistema de usuarios. Tenemos un UsuarioBase con los datos comunes, y un Admin que necesita esos datos más algunos privilegios extra.
package main
import "fmt"
// Struct base con funcionalidad común
type UsuarioBase struct {
ID int
Nombre string
}
// Método del struct base
func (u UsuarioBase) Saludar() {
fmt.Printf("Hola, soy %s (ID: %d)\n", u.Nombre, u.ID)
}
// Struct 'hijo' que INCRUSTA al padre
type Admin struct {
UsuarioBase // <--- Embedding: No le ponemos nombre al campo
Nivel int
}
func main() {
admin := Admin{
UsuarioBase: UsuarioBase{ID: 1, Nombre: "Root"},
Nivel: 5,
}
// Acceso directo a campos (promoción)
// No hace falta escribir admin.UsuarioBase.Nombre
fmt.Println(admin.Nombre)
// Acceso directo a métodos (promoción)
// Admin "hereda" el comportamiento de UsuarioBase
admin.Saludar() // Imprime: Hola, soy Root (ID: 1)
}Admin no es un UsuarioBase, pero contiene uno y puede seleccionar sus campos y métodos promovidos. Es una abreviatura sintáctica; el campo UsuarioBase sigue estando presente.
Ocultación de nombres
¿Qué pasa si el struct exterior (Admin) define un campo o método con el mismo nombre que el struct interior (UsuarioBase)?
En la herencia clásica hablaríamos de Override (Sobreescritura). En Go hablamos de Shadowing (Ocultación).
El campo del nivel más externo tiene prioridad.
type Admin struct {
UsuarioBase
Nombre string // Conflicto: UsuarioBase también tiene 'Nombre'
}
func main() {
a := Admin{
UsuarioBase: UsuarioBase{Nombre: "NombreBase"},
Nombre: "NombreAdmin",
}
fmt.Println(a.Nombre) // Imprime "NombreAdmin" (el externo gana)
fmt.Println(a.UsuarioBase.Nombre) // Imprime "NombreBase" (acceso explícito)
}Esto nos da un control total. No estamos sobreescribiendo nada por sorpresa; simplemente el campo externo “hace sombra” al interno, pero el interno sigue ahí si lo necesitamos.
«Es un» frente a «tiene un»
Este es el punto en el que los programadores de OOP clásica suelen tropezar.
En Java, si Coche extends Vehiculo, puedes hacer:
Vehiculo v = new Coche(); (Polimorfismo de clases).
En Go, ESTO NO FUNCIONA con Embedding.
func ProcesarUsuario(u UsuarioBase) {
// ...
}
func main() {
admin := Admin{}
// ERROR: cannot use admin (type Admin) as type UsuarioBase in argument to ProcesarUsuario
// ProcesarUsuario(admin)
}Aunque Admin tenga todo lo que tiene UsuarioBase, para el compilador de Go son tipos completamente distintos. Admin no es un UsuarioBase.
Cómo lograr el polimorfismo
Si no podemos pasar un Admin donde se espera un UsuarioBase, ¿cómo escribimos funciones genéricas que acepten ambos?
La respuesta es: Interfaces.
En Go, el polimorfismo no se logra por jerarquía de tipos (quién es tu padre), sino por comportamiento (qué métodos tienes). Si Admin y UsuarioBase tienen el método Saludar(), definiremos una interfaz Saludador y ambos la cumplirán.
Composición de interfaces
El Embedding no es exclusivo de los structs. Las interfaces también se pueden componer, y esto es muy común en la librería estándar de Go.
Por ejemplo, io.ReadWriter es simplemente la unión de io.Reader y io.Writer.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composición de interfaces
type ReadWriter interface {
Reader // Incrustamos la interfaz
Writer // Incrustamos la interfaz
}