
Une interface décrit un comportement, pas une donnée : « tout ce qui sait faire X ». C'est le concept qui rend un code Go testable et modulaire, en le découplant des types concrets. Ce guide vous apprend à définir des interfaces, à comprendre la satisfaction implicite propre à Go, à inspecter un type avec un type switch, et à éviter le piège vicieux de l'interface nil. Public visé : vous maîtrisez les méthodes et récepteurs. Exemples testés sur Go 1.26.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Définir une interface et l'implémenter par satisfaction implicite.
- Concevoir de petites interfaces côté consommateur.
- Inspecter un type avec une type assertion et un type switch.
- Éviter le piège de l'interface
nil.
Définir une interface : la satisfaction implicite
Section intitulée « Définir une interface : la satisfaction implicite »Une interface liste des méthodes. Un type la satisfait dès qu'il possède ces méthodes, sans le déclarer : il n'y a pas de mot-clé implements en Go. C'est la satisfaction implicite, une des grandes idées du langage.
type Notifier interface { Notifier(message string) string}
type Console struct{}func (Console) Notifier(m string) string { return "console: " + m }
type Email struct{ Dest string }func (e Email) Notifier(m string) string { return fmt.Sprintf("email->%s: %s", e.Dest, strings.ToUpper(m))}Console et Email satisfont Notifier automatiquement, simplement parce qu'ils ont une méthode Notifier(string) string. Aucun lien de déclaration entre eux : on pourrait ajouter un type Slack demain sans toucher à l'interface.
De petites interfaces, côté consommateur
Section intitulée « De petites interfaces, côté consommateur »L'idiome Go n'est pas de définir de grosses interfaces à côté des types, mais de petites interfaces là où on les consomme. Une fonction qui dépend d'un comportement déclare l'interface minimale dont elle a besoin.
// dépend du comportement, pas d'un type concretfunc alerter(n Notifier, msg string) string { return n.Notifier(msg)}
func main() { fmt.Println(alerter(Console{}, "disque plein")) fmt.Println(alerter(Email{Dest: "ops@x"}, "disque plein"))}Sortie :
console: disque pleinemail->ops@x: DISQUE PLEINalerter ne connaît ni Console ni Email : elle accepte n'importe quoi qui sait notifier. C'est ce qui la rend testable (on lui injecte un faux notifier dans un test) et extensible (on ajoute des implémentations sans la modifier). La bibliothèque standard suit ce principe : io.Writer ne déclare qu'une méthode Write, et tout l'écosystème s'y branche.
Type assertion et type switch
Section intitulée « Type assertion et type switch »Parfois, on veut récupérer le type concret derrière une interface. La type assertion n.(Email) l'extrait ; le type switch teste plusieurs types d'un coup.
var n Notifier = Email{Dest: "a@b"}switch v := n.(type) {case Email: fmt.Println("c'est un Email pour", v.Dest)case Console: fmt.Println("c'est une Console")}Le type switch est plus sûr qu'une cascade d'assertions : chaque case fournit la variable v déjà convertie au bon type. On l'utilise avec parcimonie, car dépendre du type concret va à l'encontre du découplage ; mais c'est utile pour traiter des cas particuliers.
Le piège de l'interface nil
Section intitulée « Le piège de l'interface nil »Voici l'un des pièges les plus vicieux de Go. Une interface contient deux choses : un type et une valeur. Elle n'est nil que si les deux le sont. Renvoyer un pointeur concret nil comme interface donne une interface non-nil.
type MonErreur struct{}func (*MonErreur) Error() string { return "boum" }
func mauvais() error { var p *MonErreur // p est nil return p // mais l'interface error porte le type *MonErreur}
func main() { fmt.Println(mauvais() == nil) // false : surprise !}mauvais() renvoie un pointeur nil, mais l'interface error retournée n'est pas nil car elle mémorise le type *MonErreur. Le test err == nil échoue et le code croit à une erreur inexistante. La règle : renvoyez explicitement nil (le mot-clé, pas une variable pointeur nil) quand il n'y a pas d'erreur.
Exercice : additionner des aires
Section intitulée « Exercice : additionner des aires »Un exercice qui montre la puissance des interfaces. Cherchez la solution avant d'ouvrir la réponse.
Définissez une interface Forme avec une seule méthode Aire() float64, puis :
- un type
Cercle(champR) et un typeRectangle(champsL,H), chacun implémentantAire(). - une fonction
aireTotale(formes []Forme) float64qui additionne les aires de n'importe quelles formes.
Vérifiez avec un cercle de rayon 1 et un rectangle 2×3 (total attendu : environ 9,14).
Indice : aireTotale ne doit connaître que l'interface Forme, pas les types concrets.
aireTotale itère sur des Forme : elle fonctionne pour tout type qui sait calculer son aire, présent ou futur.
package main
import ( "fmt" "math")
type Forme interface { Aire() float64}
type Cercle struct{ R float64 }func (c Cercle) Aire() float64 { return math.Pi * c.R * c.R }
type Rectangle struct{ L, H float64 }func (r Rectangle) Aire() float64 { return r.L * r.H }
func aireTotale(formes []Forme) float64 { var total float64 for _, f := range formes { total += f.Aire() } return total}
func main() { formes := []Forme{Cercle{R: 1}, Rectangle{L: 2, H: 3}} fmt.Printf("aire totale: %.2f\n", aireTotale(formes))}Résultat :
aire totale: 9.14Pour ajouter un Triangle, il suffit de lui donner une méthode Aire() : aireTotale le prend en charge sans aucune modification. C'est exactement le pattern qu'on retrouve dans les outils réels, où un moteur traite une liste de composants découplés par une interface.
À retenir
Section intitulée « À retenir »- Une interface décrit un comportement (une liste de méthodes), pas une donnée.
- La satisfaction est implicite : un type satisfait une interface dès qu'il a les bonnes méthodes, sans le déclarer.
- Préférez de petites interfaces côté consommateur (comme
io.Writer) : le code devient testable et extensible. - Le type switch récupère le type concret, à utiliser avec parcimonie.
- Piège : une interface
nilexige type ET valeurnil; renvoyez le mot-clénil, jamais un pointeurnil.