AI News Hub Logo

AI News Hub

¿Por qué Go mató a la herencia?

DEV Community
Juan Carlos Garcia Esquivel

La decisión de los creadores de Go de eliminar la herencia de clases no fue un capricho académico. Fue una respuesta directa a décadas de jerarquías de objetos frágiles. En Go, no heredas lo que un objeto "es", sino que construyes lo que un objeto "hace". La herencia tradicional crea acoplamiento rígido (Is-a)(es un). La composición en Go utiliza el Embedding para crear sistemas flexibles (Has-a)(tiene un), evitando el problema de la clase base frágil y permitiendo una evolución del código mucho más orgánica. Imagina que vas a un restaurante. La herencia es como un Menú Ejecutivo Fijo: si pides el "Combo A", estás obligado a recibir la sopa, el plato fuerte y el café. Aunque no te guste la sopa, la tienes en tu mesa porque "eres" un cliente del Combo A. El hijo (clase hija) está obligado a cargar con todo el equipaje del padre (clase base). La composición es como pedir A la Carta: tú solo pides la hamburguesa y la malteada. Si luego necesitas papas, las pides por separado. No tienes que cargar con una sopa que no pediste solo para llegar al plato fuerte. En Go, solo "pides" (compones) las piezas de código que realmente vas a usar. Es cuando incluyes un tipo dentro de una struct sin asignarle un nombre de campo. Go permite embeber tanto structs como interfaces. Es la capacidad de Go de "elevar" los métodos de un tipo embebido a la superficie del tipo que lo contiene. Esto hace que los métodos del nivel interno estén disponibles directamente en el nivel externo. Para el usuario del código, parece que la struct raíz tiene esos métodos, aunque internamente Go solo esté delegando la llamada. Aquí obtienes la lógica interna del componente de forma estática gracias a la promoción. type Logger struct{} func (l Logger) Log(msg string) { fmt.Println(msg) } type User struct { Logger // Anónimo: los métodos de Logger se promocionan a User } u := User{} u.Log("Promoción en acción") // Llamada directa gracias a la promoción Esto crea un "slot" vacío que debe ser llenado con un objeto real en tiempo de ejecución. type Speaker interface { Speak() string } // Humano implementa Speaker de forma implícita type Humano struct { Nombre string } func (h Humano) Speak() string { return "Hola, soy " + h.Nombre } type Robot struct { Speaker // Slot para cualquier cosa que satisfaga el contrato } // Llenamos el slot con un humano r := Robot{ Speaker: Humano{Nombre: "Alex"} } fmt.Println(r.Speak()) // Delegación al objeto en el slot Mecánica de Ejecución: Cuando llamas a r.Speak(), Go busca en el campo oculto llamado Speaker. Si hay un objeto ahí, le delega la llamada. Si el campo está vacío (nil), el programa lanzará un pánico. Si le asignas un nombre al campo, dejas de usar "Embedding" y pasas a una relación de pertenencia tradicional. type User struct { Service Logger // Campo nombrado: NO hay promoción Name string } // Uso: u := User{Name: "Alex"} u.Service.Log("Acceso explícito") // OBLIGATORIO usar el nombre del campo // u.Log("Error") -> Esto NO compilaría Sin Promoción: Al darle un nombre (ej. Service), los métodos se quedan "encerrados" dentro de ese atributo. El compilador ya no los sube a la superficie de User. Encapsulamiento: Es la forma ideal de usar un componente internamente sin "contaminar" la API pública de tu struct. ¿Qué pasa si intentas combinar dos "combos" que traen el mismo plato? type Amazon struct{} func (a Amazon) Pay() { fmt.Println("Pago con Amazon") } type MercadoLibre struct{} func (m MercadoLibre) Pay() { fmt.Println("Pago con MELI") } type Store struct { Amazon MercadoLibre } // store.Pay() -> Error: "ambiguous selector" Definición vs. Llamada: Es perfectamente válido definir una struct que embeba múltiples tipos con métodos idénticos. El compilador no dará error al definir la estructura. El Error "Ambiguous Selector": El error solo ocurre en el momento de la llamada si intentas usar la promoción (ej. store.Pay()). Como Go no puede adivinar, detiene la ejecución. La Solución: Debes ser explícito y navegar a través del tipo interno: store.Amazon.Pay(). La promoción es recursiva. Si el nivel N tiene algo, el nivel N+1 lo hereda. type A struct{} func (a A) MetodoA() {} type B struct { A } // B tiene MetodoA por promoción type C struct { B } // C tiene MetodoA por promoción var obj C obj.MetodoA() // Funciona perfectamente A diferencia de Java o C#, en Go no existe el keyword implements. Las interfaces se satisfacen implícitamente. La prueba del pato "Si camina como un pato y grazna como un pato, entonces es un pato". Esto permite que puedas crear una interfaz hoy para tipos que fueron escritos hace años, logrando un desacoplamiento total entre el que define el contrato y el que lo cumple.