Embedding en un ejemplo
Un campo con tipo pero sin nombre es un campo incrustado. Los campos y métodos del tipo incrustado pasan a ser accesibles directamente en el tipo exterior.
Salida:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
El nombre del campo incrustado es el nombre de su tipo, User. Así es como te refieres a él en los literales (User: User{...}) y cuando necesitas el propio valor interior (a.User). Un campo promovido no se puede asignar por su nombre corto en un literal: Admin{Name: "Ana"} falla con unknown field Name in struct literal of type Admin.
Los métodos promovidos cumplen interfaces
La promoción añade los métodos del tipo incrustado al conjunto de métodos del tipo exterior. Así que el tipo exterior cumple todas las interfaces que cumple el tipo interior.
Las reglas de los conjuntos de métodos siguen al tipo incrustado. Incrustar T promueve los métodos con receptor por valor de T tanto al valor exterior como al puntero exterior, pero los métodos con receptor puntero de *T solo al puntero exterior. Incrustar *T promueve los dos conjuntos a ambos. En caso de duda, incrusta el puntero, o usa el tipo exterior a través de un puntero.
Incrustar un puntero tiene un coste: el puntero puede ser nil. Service{} sin Logger compila, y svc.Log(...) provoca entonces un panic.
El embedding no es herencia
El embedding parece herencia de clases, pero tres cosas se comportan distinto que en Java o Python.
El tipo exterior no es el tipo interior. Un Admin no es un User. Una función que recibe un User no acepta un Admin; pasa a.User.
No hay despacho virtual. Un método promovido se ejecuta sobre el valor incrustado y no sabe nada del tipo exterior. Si llama a otro método, llama a la versión de su propio tipo, aunque el tipo exterior defina uno con el mismo nombre.
Salida:
woof
The animal says ...
En un lenguaje basado en clases, Speak imprimiría "woof". En Go, d.Speak() es una abreviatura de d.Animal.Speak(), y el receptor de esa llamada es el Animal. Cuando quieras un comportamiento que varíe según el tipo, usa una interfaz: pasa un Sounder al código que lo necesita.
El tipo interior no sabe que está incrustado. No hay super. Para ampliar un método promovido, define en el tipo exterior un método con el mismo nombre y llama explícitamente al interior:
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
Conflictos de nombres y sombreado
Un campo o un método del tipo exterior tapa a uno promovido con el mismo nombre, como hizo Dog.Sound más arriba. Gana el nombre menos profundo.
Cuando dos tipos incrustados a la misma profundidad promueven el mismo nombre, ese nombre se vuelve ambiguo. El programa sigue compilando mientras nada use el nombre ambiguo:
Resuelve el conflicto usando la ruta completa, o definiendo ID en el tipo exterior.
Incrustar interfaces
Las interfaces pueden incrustar otras interfaces. La librería estándar construye así las interfaces más grandes:
type ReadWriter interface {
Reader
Writer
}
Un struct también puede incrustar una interfaz. El struct cumple entonces esa interfaz a través del valor que guardes en el campo, y sobrescribes solo los métodos que te interesan. Es el patrón decorador casi sin código:
El c.Reader.Read(p) explícito es obligatorio. Escribir c.Read(p) dentro de Read se llamaría a sí mismo para siempre.
Incrustar una interfaz en un doble de test es un atajo habitual: incrustas la interfaz grande, implementas el único método que necesita el test y dejas el resto. Cualquier método sin implementar provoca un panic por desreferencia de un puntero nil si se llama, que en un test suele ser lo que quieres.
Un uso real habitual: incrustar sync.Mutex
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
Se lee bien, pero también exporta Lock y Unlock como parte de la API de Stats, así que cualquiera puede bloquear tu struct. Para tipos que se usan fuera del paquete, un campo con nombre (mu sync.Mutex) mantiene privado el lock. Esa contrapartida vale para todo embedding: todo lo que exporta el tipo incrustado pasa a formar parte de la superficie pública de tu tipo.
Errores comunes
- Esperar despacho virtual. Los métodos promovidos nunca llaman a las versiones sobrescritas del tipo exterior.
- Asignar campos promovidos en un literal. Usa el nombre del tipo incrustado:
Admin{User: User{Name: "Ana"}}. - Punteros incrustados nil. Un
*To una interfaz incrustados tienen que estar asignados antes de llamar a sus métodos. - Superficie de API accidental. El embedding exporta en tu tipo todos los métodos exportados del tipo interior.
Preguntas frecuentes
¿Qué es el embedding de structs en Go?
Declarar un campo solo con un tipo y sin nombre: type Admin struct { User; Level int }. Los campos y métodos del User incrustado se promueven, así que a.Name y a.Greet() funcionan directamente sobre un Admin. El valor incrustado sigue siendo un campo normal, accesible como a.User.
¿Tiene Go herencia?
No. Go no tiene clases ni herencia de subtipos. El embedding da reutilización de código por composición: el tipo exterior recibe los métodos del tipo interior, pero un Admin no es un User. No puedes pasar un Admin donde se espera un User, y los métodos promovidos no pueden llamar a los métodos del tipo exterior. En Go el polimorfismo viene de las interfaces.
¿Cómo se inicializa un struct incrustado en Go?
En un literal compuesto, nombra el campo incrustado por el nombre de su tipo: Admin{User: User{Name: "Ana"}, Level: 2}. No puedes asignar campos promovidos directamente en el literal: Admin{Name: "Ana"} falla con unknown field Name in struct literal of type Admin.
¿Se puede incrustar una interfaz en un struct en Go?
Sí. El struct cumple entonces la interfaz a través del valor incrustado, y puedes sobrescribir métodos concretos. Es habitual en decoradores y dobles de test. Si el campo de interfaz incrustado es nil, llamar a un método que no sobrescribiste provoca un panic por desreferencia de un puntero nil.