Menu

Embedding de structs en Golang : champs et méthodes promus

L'embedding place un type dans un autre sans nom de champ, si bien que ses champs et ses méthodes sont promus vers le type extérieur. Découvrez comment fonctionne la promotion, l'embedding d'interfaces, les conflits de noms, et pourquoi l'embedding n'est pas de l'héritage.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

L'embedding en un exemple

Un champ avec un type mais sans nom est un champ embarqué. Les champs et les méthodes du type embarqué deviennent accessibles directement sur le type extérieur.

Sortie :

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

Le nom du champ embarqué est le nom de son type, User. C'est ainsi que vous y faites référence dans les littéraux (User: User{...}) et quand vous avez besoin de la valeur intérieure elle-même (a.User). Un champ promu ne peut pas être défini par son nom court dans un littéral : Admin{Name: "Ana"} échoue avec unknown field Name in struct literal of type Admin.

Les méthodes promues satisfont des interfaces

La promotion ajoute les méthodes du type embarqué à l'ensemble de méthodes du type extérieur. Le type extérieur satisfait donc toutes les interfaces que satisfait le type intérieur.

Les règles d'ensemble de méthodes suivent le type embarqué. Embarquer T promeut les méthodes à receveur valeur de T à la fois vers la valeur extérieure et vers le pointeur extérieur, mais les méthodes à receveur pointeur de *T uniquement vers le pointeur extérieur. Embarquer *T promeut les deux ensembles vers les deux. Dans le doute, embarquez le pointeur, ou utilisez le type extérieur via un pointeur.

Embarquer un pointeur a un coût : le pointeur peut être nil. Service{} sans Logger compile, et svc.Log(...) provoque alors un panic.

L'embedding n'est pas de l'héritage

L'embedding ressemble à la création de sous-classes, mais trois choses se comportent différemment de Java ou Python.

Le type extérieur n'est pas le type intérieur. Un Admin n'est pas un User. Une fonction qui prend un User n'acceptera pas un Admin ; passez a.User.

Il n'y a pas de dispatch virtuel. Une méthode promue s'exécute sur la valeur embarquée, et elle ne sait rien du type extérieur. Si elle appelle une autre méthode, elle appelle la version de son propre type, même quand le type extérieur en définit une du même nom.

Sortie :

woof
The animal says ...

Dans un langage à classes, Speak afficherait « woof ». En Go, d.Speak() est un raccourci pour d.Animal.Speak(), et le receveur de cet appel est l'Animal. Quand vous voulez un comportement qui varie selon le type, utilisez une interface : passez un Sounder au code qui en a besoin.

Le type intérieur ne sait pas qu'il est embarqué. Il n'y a pas de super. Pour étendre une méthode promue, définissez une méthode du même nom sur le type extérieur et appelez explicitement celle du type intérieur :

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

Conflits de noms et masquage

Un champ ou une méthode du type extérieur masque un élément promu du même nom, comme Dog.Sound l'a fait plus haut. Le nom le moins profond l'emporte.

Quand deux types embarqués à la même profondeur promeuvent le même nom, ce nom devient ambigu. Le programme compile tant que rien n'utilise le nom ambigu :

Résolvez le conflit en utilisant le chemin complet, ou en définissant ID sur le type extérieur.

Embarquer des interfaces

Les interfaces peuvent embarquer d'autres interfaces. La bibliothèque standard construit ainsi des interfaces plus larges :

type ReadWriter interface {
	Reader
	Writer
}

Une struct peut aussi embarquer une interface. La struct satisfait alors cette interface grâce à la valeur stockée dans le champ, et vous ne redéfinissez que les méthodes qui vous intéressent. C'est le motif décorateur avec presque pas de code :

Le c.Reader.Read(p) explicite est obligatoire. Écrire c.Read(p) dans Read s'appellerait lui-même indéfiniment.

Embarquer une interface dans une doublure de test est un raccourci courant : embarquez la grosse interface, implémentez la seule méthode dont le test a besoin, et laissez le reste. Toute méthode non implémentée provoque un panic de déréférencement de pointeur nil si elle est appelée, ce qui est souvent ce que vous voulez dans un test.

Un usage réel courant : embarquer 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]++
}

Cela se lit bien, mais cela exporte aussi Lock et Unlock dans l'API de Stats, donc n'importe quel appelant peut verrouiller votre struct. Pour les types utilisés hors du package, un champ nommé (mu sync.Mutex) garde le verrou privé. Ce compromis vaut pour tout embedding : tout ce que le type embarqué exporte fait partie de la surface publique de votre type.

Erreurs courantes

  • S'attendre à un dispatch virtuel. Les méthodes promues n'appellent jamais les redéfinitions du type extérieur.
  • Définir des champs promus dans un littéral. Utilisez le nom du type embarqué : Admin{User: User{Name: "Ana"}}.
  • Des pointeurs embarqués nil. Un *T ou une interface embarqués doivent être définis avant qu'on appelle leurs méthodes.
  • Une surface d'API involontaire. L'embedding exporte sur votre type toutes les méthodes exportées du type intérieur.

Questions fréquentes

Qu'est-ce que l'embedding de structs en Go ?

Déclarer un champ avec seulement un type et pas de nom : type Admin struct { User; Level int }. Les champs et les méthodes du User embarqué sont promus, donc a.Name et a.Greet() fonctionnent directement sur un Admin. La valeur embarquée reste un champ normal, accessible via a.User.

Go a-t-il l'héritage ?

Non. Go n'a ni classes ni héritage de sous-type. L'embedding permet la réutilisation de code par composition : le type extérieur obtient les méthodes du type intérieur, mais un Admin n'est pas un User. Vous ne pouvez pas passer un Admin là où un User est attendu, et les méthodes promues ne peuvent pas rappeler les méthodes du type extérieur. Le polymorphisme en Go passe par les interfaces.

Comment initialiser une struct embarquée en Go ?

Dans un littéral composite, nommez le champ embarqué par le nom de son type : Admin{User: User{Name: "Ana"}, Level: 2}. Vous ne pouvez pas définir directement les champs promus dans le littéral : Admin{Name: "Ana"} échoue avec unknown field Name in struct literal of type Admin.

Peut-on embarquer une interface dans une struct en Go ?

Oui. La struct satisfait alors l'interface grâce à la valeur embarquée, et vous pouvez redéfinir certaines méthodes. C'est courant pour les décorateurs et les doublures de test. Si le champ interface embarqué vaut nil, appeler une méthode que vous n'avez pas redéfinie provoque un panic de déréférencement de pointeur nil.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER