Menu

Interfaces en Golang : implémentation implicite, any, piège du nil

Les interfaces Go sont satisfaites implicitement : tout type qui a les bonnes méthodes les implémente. Découvrez les petites interfaces comme io.Reader et fmt.Stringer, l'interface vide any, le piège de l'interface nil, et le principe « acceptez des interfaces, renvoyez des structs ».

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

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 :

InterfaceMéthodeUtilisée par
fmt.StringerString() stringl'affichage de fmt
errorError() stringtoute fonction qui peut échouer
io.ReaderRead(p []byte) (n int, err error)fichiers, réseau, gzip, bodies HTTP
io.WriterWrite(p []byte) (n int, err error)fichiers, buffers, hachages, réponses HTTP
sort.InterfaceLen, Less, Swaple package sort
http.HandlerServeHTTP(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.Open renvoie un *os.File, pas un io.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 nil litté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.Reader n'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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER