En el lenguaje Go, el soporte para la composición se implementa a través del mecanismo de embedding: es la capacidad de incluir una estructura dentro de otra sin un nombre de campo explícito. Se supone que este enfoque permitirá modelar la herencia "a su manera", mientras se conserva la simplicidad del lenguaje y se evita muchas complicaciones relacionadas con la herencia múltiple.
A menudo, los desarrolladores desean extender el comportamiento o la interfaz de alguna estructura base sin complicar la arquitectura. En Go, se utiliza a menudo el embedding, lo que permite acceder a los métodos y campos del tipo anidado directamente, como si estuvieran definidos en la estructura padre. Pero el embedding tiene sus propias características, y una comprensión incorrecta del mecanismo puede conducir a errores inesperados, como conflictos de nombres, doble embedding y herencia incorrecta de métodos.
El uso adecuado y económico del embedding permite lograr una composición más limpia. Se debe recordar que los métodos y campos se "elevan" solo un nivel y que el embedding implementa exactamente la relación "has-a", no "is-a". Es necesario evitar conflictos de nombres y ser consciente de cómo funciona el método receptor.
Ejemplo de código:
package main import "fmt" type Engine struct { Power int } func (e Engine) Start() { fmt.Println("Motor iniciado con potencia", e.Power) } type Car struct { Engine // embedding, no campo Engine como engine Engine Brand string } func main() { c := Car{Engine: Engine{Power: 200}, Brand: "Toyota"} c.Start() // acceso directo fmt.Println(c.Power) // el campo también está elevado }
Características clave:
¿Puede el embedding realizar herencia múltiple?
No, el embedding no realiza herencia como en OOP, es una composición. Se pueden embeder múltiples estructuras dentro de una, pero al coincidir los métodos, surge un conflicto de ensamblaje, no una fusión.
¿Qué pasa si los campos de las estructuras anidadas tienen los mismos nombres?
Se producirá un error de compilación al intentar acceder directamente: el compilador no sabe a qué campo dirigirse. Es necesario especificar el camino de manera explícita a través del nombre de la estructura anidada.
Ejemplo de código:
type A struct {X int} type B struct {X int} type C struct { A B } func main() { c := C{} // c.X = 1 // error: selector ambiguo c.A.X = 1 // solo así }
¿Se elevan los métodos con valor/puntero de la misma manera en el embedding?
No. Los métodos con receptor de puntero solo se elevan si la estructura también usa puntero (c := &Car{}). Si la estructura es por valor, los métodos con receptor de puntero no se "elevarán".
Proyecto con una estructura profundamente anidada, donde se usa el embedding para simular herencia multinivel. Un novato no entiende de qué estructura proviene un campo o método, todo el proyecto se complica y se vuelve "mágico".
Ventajas:
Desventajas:
Se utiliza el embedding para implementar una interfaz, por ejemplo, la interfaz Logger se implementa a través del tipo anidado con el método Println, y esto está claramente documentado y cubierto con pruebas.
Ventajas:
Desventajas: