Une interface est un ensemble de méthodes
Un type interface liste des signatures de méthodes. Tout type qui possède ces méthodes satisfait l'interface, sans aucune déclaration pour le dire.
Ni Rect ni Circle ne mentionnent Shape. C'est l'implémentation implicite, la caractéristique déterminante des interfaces Go. Vous pouvez ainsi définir dans votre package une interface que des types d'autres packages satisfont déjà, sans toucher à leur code.
Les petites interfaces de la bibliothèque standard
Le code Go privilégie les interfaces à une ou deux méthodes. Les plus importantes :
| Interface | Méthode | Utilisée par |
|---|---|---|
fmt.Stringer | String() string | l'affichage de fmt |
error | Error() string | toute fonction qui peut échouer |
io.Reader | Read(p []byte) (n int, err error) | fichiers, réseau, gzip, bodies HTTP |
io.Writer | Write(p []byte) (n int, err error) | fichiers, buffers, hachages, réponses HTTP |
sort.Interface | Len, Less, Swap | le package sort |
http.Handler | ServeHTTP(w, r) | net/http |
Comme io.Reader n'a qu'une méthode, des dizaines de types l'implémentent, et toute fonction qui prend un io.Reader fonctionne avec tous :
Le proverbe Go dit : « plus l'interface est grande, plus l'abstraction est faible ». Les interfaces plus larges se construisent en combinant de petites : io.ReadWriter est Reader plus Writer, écrit en embarquant une interface dans une autre.
any : l'interface vide
interface{} n'a aucune méthode, donc tous les types la satisfont. Go 1.18 a ajouté any comme alias ; les deux sont identiques.
Une valeur any peut contenir n'importe quoi, mais vous ne pouvez presque rien en faire tant que vous n'avez pas récupéré le type concret avec une assertion de type ou un type switch. Préférez une vraie interface ou les génériques quand l'ensemble des types est connu. any convient aux données vraiment dynamiques, comme du JSON décodé de forme inconnue, et à l'affichage.
Ce que contient une valeur interface
Une valeur interface est une paire : un type dynamique et une valeur dynamique. var s Shape = Rect{3, 4} stocke le type Rect et une copie de la valeur. Appeler s.Area() recherche la méthode de Rect à l'exécution.
Une interface n'est nil que lorsque les deux parties sont vides. Cette règle cause le bug le plus déroutant de Go.
Le piège de l'interface nil
Un pointeur nil stocké dans une interface donne une interface non nil.
Sortie :
false
*main.MyError true
true
validate(true) renvoie une interface error contenant le type *MyError et la valeur nil. L'interface a un type, donc elle n'est pas égale à nil, et la branche if err != nil de l'appelant s'exécute. Appeler err.Error() à cet endroit provoquerait alors un panic lors de l'accès au champ du receveur nil.
La correction est simple : déclarez la variable comme error, pas comme le type pointeur concret, ou renvoyez un nil littéral sur le chemin de succès. Ne renvoyez jamais un type pointeur d'erreur concret depuis une fonction dont le résultat est error. Le même piège vaut pour toute interface, pas seulement les erreurs.
Vérifier qu'un type implémente une interface
L'implémentation est vérifiée là où une valeur est affectée à une interface. Si aucun code ne le fait encore, une erreur dans une signature de méthode passe inaperçue. Une affectation à l'identifiant blanc au niveau du package rend la vérification explicite :
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
Cela ne coûte rien à l'exécution. Si *LogWriter a Write(p []byte) error au lieu de Write(p []byte) (int, error), le build échoue :
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Receveurs pointeurs et interfaces
Si une méthode a un receveur pointeur, seul le type pointeur possède cette méthode. *Counter satisfait une interface grâce à elle ; Counter non. Le compilateur indique Counter does not implement Incrementer (method Inc has pointer receiver). Stockez &Counter{} dans l'interface. La page sur les méthodes explique les ensembles de méthodes.
Acceptez des interfaces, renvoyez des structs
Une recommandation Go courante : les fonctions devraient prendre des paramètres interface et renvoyer des types concrets.
- Accepter une interface permet aux appelants de passer tout ce qui convient, y compris des doublures de test. Une fonction qui lit des données devrait prendre un
io.Reader, pas un*os.File. - Renvoyer un type concret permet aux appelants d'utiliser toutes ses méthodes et tous ses champs, et évite le piège de l'interface nil.
os.Openrenvoie un*os.File, pas unio.Reader.
Une habitude liée : définissez les interfaces là où elles sont utilisées, pas là où elles sont implémentées. Si votre service a besoin de quelque chose qui peut faire Get(id) sur un utilisateur, déclarez une interface à une méthode dans le package de votre service, et laissez le package de base de données exporter simplement sa struct.
Comparer des valeurs interface
Deux valeurs interface sont égales quand leurs types dynamiques sont identiques et leurs valeurs dynamiques égales. Si le type dynamique n'est pas comparable (une slice, une map), == compile mais provoque un panic à l'exécution : runtime error: comparing uncomparable type []int.
Erreurs courantes
- Renvoyer un pointeur nil typé sous forme d'interface. Renvoyez un
nillittéral. - Des interfaces trop tôt. Écrivez d'abord le type concret. Ajoutez une interface quand une deuxième implémentation ou un test en a besoin.
- Un pointeur vers une interface.
*io.Readern'est presque jamais correct. Une interface contient déjà un pointeur quand vous y en stockez un. - De grosses interfaces. Les interfaces à dix méthodes sont difficiles à implémenter et à simuler. Découpez-les.
Questions fréquentes
Comment implémenter une interface en Go ?
Définissez sur votre type les méthodes que l'interface liste, avec les mêmes noms et signatures. Il n'y a pas de mot-clé implements. Si *File a Read(p []byte) (int, error), c'est un io.Reader, automatiquement. Le compilateur le vérifie partout où vous affectez la valeur au type interface.
Qu'est-ce que l'interface vide, ou any, en Go ?
interface{} n'a aucune méthode, donc tous les types la satisfont. Depuis Go 1.18, any est un alias intégré de interface{}. Une valeur de type any peut contenir n'importe quoi, mais il faut une assertion de type ou un type switch pour en ressortir un type concret.
Pourquoi mon interface Go n'est-elle pas nil alors que j'y ai affecté un pointeur nil ?
Une valeur interface contient un type et une valeur. Affecter un *MyError nil à une error donne une interface dont le type est *MyError et la valeur nil, et cette interface n'est pas égale à nil. Renvoyez un nil littéral au lieu d'un pointeur nil typé quand il n'y a pas d'erreur.
Comment vérifier à la compilation qu'un type implémente une interface ?
Ajoutez une affectation à l'identifiant blanc au niveau du package : var _ io.Reader = (*MyReader)(nil). S'il manque une méthode à *MyReader, le build échoue avec un message qui nomme la méthode manquante.