backend / go
Interfaces
Explication
Ce que vous allez apprendre
- Comprendre le typage structurel de Go : une interface satisfaite sans mot-clé
implements - Écrire une fonction polymorphe qui accepte n'importe quel type respectant une interface
- Utiliser
any(interface vide) avec parcimonie, et savoir pourquoi - Extraire le type concret d'une interface avec une type assertion et un type switch
- Comprendre pourquoi la bibliothèque standard utilise
Stringeret des interfaces à une méthode
Dans quel contexte ?
Un développeur écrit une fonction calculerFacture(f Forme) qui doit fonctionner aussi bien avec un Rectangle qu'avec un Cercle, deux types qu'il n'a pas encore écrits au moment où il conçoit cette fonction. En Java, il faudrait déclarer explicitement class Rectangle implements Forme. En Go, il suffit que Rectangle possède les bonnes méthodes : aucune déclaration d'intention n'est requise.
D'abord, une interface n'est qu'un ensemble de signatures de méthodes
type Forme interface { Aire() float64; Perimetre() float64 } ne décrit aucune donnée, seulement un contrat comportemental. N'importe quel type qui possède ces deux méthodes, avec exactement ces signatures, satisfait automatiquement l'interface — c'est ce qu'on appelle le typage structurel, ou "duck typing" statique.
Cette approche change profondément la façon de concevoir du code réutilisable : plutôt que de définir une interface puis de forcer chaque type à déclarer qu'il l'implémente, en Go on écrit d'abord des types concrets, et les interfaces émergent ensuite pour capturer les comportements communs déjà présents.
Une fois ce principe compris, un idiome Go célèbre prend tout son sens
"Accept interfaces, return structs" (accepte des interfaces, retourne des structs) résume la philosophie Go : une fonction devrait accepter le type le plus général possible en paramètre (une petite interface), mais retourner un type concret précis, plus simple à utiliser pour l'appelant.
| Concept | Exemple | Usage |
|---|---|---|
| Interface classique | type Forme interface { Aire() float64 } | Regrouper un comportement métier |
| Interface vide | any (alias de interface{}) | Accepter absolument n'importe quel type |
| Interface à une méthode | type Stringer interface { String() string } | Standard très utilisé par fmt |
Prérequis
Il faut être à l'aise avec les structs et les méthodes (leçon précédente) : une interface n'a de sens que si tu comprends déjà comment une méthode est attachée à un type.
Il reste un usage à manier avec précaution : l'interface vide
any (introduit comme alias de interface{} depuis Go 1.18) accepte littéralement n'importe quelle valeur, ce qui semble pratique mais fait perdre toute garantie de type à la compilation. Une fonction qui prend any en paramètre doit ensuite "redécouvrir" le type réel avec une assertion ou un type switch, ce qui déplace les erreurs potentielles de la compilation vers l'exécution.
Piège fréquent
Utiliser any par facilité là où une interface précise (ou des generics, vus plus loin dans ce cours) suffirait revient à abandonner l'un des plus gros avantages de Go : la vérification de type à la compilation. Réserve any aux cas où le type est vraiment inconnu à l'avance, comme le décodage JSON générique.
Enfin, comment récupérer le type concret depuis une interface
La type assertion v, ok := f.(Rectangle) tente de récupérer le type concret Rectangle depuis une variable d'interface f, sans jamais paniquer grâce au ok en second retour. Le type switch (switch v := val.(type) { case int: ... }) fait la même chose pour plusieurs types possibles d'un coup.
Bonne pratique
Préfère toujours des interfaces petites, avec une ou deux méthodes maximum (comme io.Reader ou Stringer dans la stdlib). Une interface avec dix méthodes est difficile à satisfaire et signale souvent une conception trop rigide.
Maintenant que tu sais écrire du code polymorphe, la prochaine leçon approfondit la structure de données la plus utilisée de Go au quotidien : le slice, déjà croisé dans les exemples précédents.
Commandes & code
Interfaces
Le typage structurel de Go : une interface est satisfaite implicitement, sans mot-clé implements.
package main
import (
"fmt"
"math"
)
// Interface = ensemble de methodes. N'importe quel type qui les possede
// satisfait l'interface, sans declaration explicite.
type Forme interface {
Aire() float64
Perimetre() float64
}
type Rectangle struct {
Largeur, Hauteur float64
}
func (r Rectangle) Aire() float64 { return r.Largeur * r.Hauteur }
func (r Rectangle) Perimetre() float64 { return 2 * (r.Largeur + r.Hauteur) }
type Cercle struct {
Rayon float64
}
func (c Cercle) Aire() float64 { return math.Pi * c.Rayon * c.Rayon }
func (c Cercle) Perimetre() float64 { return 2 * math.Pi * c.Rayon }
// Fonction polymorphe : accepte toute Forme, sans connaitre le type concret
func decrire(f Forme) string {
return fmt.Sprintf("aire=%.2f perimetre=%.2f", f.Aire(), f.Perimetre())
}
// Interface vide : accepte n'importe quel type (equivalent "any" depuis Go 1.18)
func afficherTout(valeurs ...any) {
for _, v := range valeurs {
fmt.Println(v)
}
}
// Petite interface standard tres utilisee
type Stringer interface {
String() string
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(%vx%v)", r.Largeur, r.Hauteur)
}
func main() {
formes := []Forme{
Rectangle{Largeur: 4, Hauteur: 5},
Cercle{Rayon: 3},
}
for _, f := range formes {
fmt.Println(decrire(f))
}
afficherTout(1, "deux", 3.0, true)
var f Forme = Rectangle{Largeur: 2, Hauteur: 3}
fmt.Println(f) // fmt utilise automatiquement String() si dispo (Stringer)
// Type assertion : recuperer le type concret depuis une interface
if rect, ok := f.(Rectangle); ok {
fmt.Println("c'est bien un rectangle :", rect.Largeur)
}
// Type switch : brancher sur le type concret
var val any = 42
switch v := val.(type) {
case int:
fmt.Println("entier :", v)
case string:
fmt.Println("chaine :", v)
default:
fmt.Println("type inconnu")
}
}Résumé
- Une interface est satisfaite implicitement : aucun
implementsrequis. - Préférer des interfaces petites (1-2 méthodes) — "accept interfaces, return structs".
any(alias deinterface{}) accepte tout type, à utiliser avec parcimonie.- Type assertion
v.(T)et type switchv.(type)extraient le type concret.
Exercices pratiques
Mission : une erreur qui n'est jamais nil, même quand elle devrait l'être
Objectif : Sécuriser une type assertion qui panique en production, puis comprendre pourquoi une interface peut sembler non-nil alors qu'elle semble vide.
Contexte
Le service de facturation utilise une fonction traiter(f Forme) qui fait rect := f.(Rectangle) sans vérifier le second retour, en supposant que seules des Rectangle seront passées. Depuis qu'un Cercle a été ajouté au catalogue, le service plante en production avec un panic à chaque commande impliquant un cercle.