composicion-vs-herencia-embedding-structs

Composición en Go con embedding y reutilización de structs

  • 4 min

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)
}
Copied!

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)
}
Copied!

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)
}
Copied!

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
}
Copied!