Une interface en Go est un ensemble de signatures de méthodes. Tout type qui possède ces méthodes satisfait automatiquement l’interface : vous décrivez ce qu’une valeur sait faire, pas ce qu’elle est.
Déclarer une interface
Vous déclarez une interface avec type ... interface et listez les méthodes
qu’elle exige. Par convention, les interfaces à une seule méthode se terminent
souvent par « -er ».
// Shape est tout ce qui peut renvoyer son aire.
type Shape interface {
Area() float64
}
La satisfaction implicite
Il n’existe pas de mot-clé implements en Go. Un type satisfait une interface
simplement en possédant les méthodes requises. Ce découplage permet à un type de
satisfaire une interface définie plus tard, même dans un autre package.
type Circle struct {
Radius float64
}
// Comme Circle possède une méthode Area() float64, c'EST un Shape.
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
Un exemple complet
Le programme ci-dessous range différents types concrets dans un []Shape et
calcule un total sans se soucier de quelle forme est laquelle. Il compile et
s’exécute tel quel.
package main
import (
"fmt"
"math"
)
// Shape est tout ce qui peut renvoyer son aire.
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// totalArea fonctionne avec n'importe quel Shape, quel que soit son type concret.
func totalArea(shapes []Shape) float64 {
var sum float64
for _, s := range shapes {
sum += s.Area()
}
return sum
}
func main() {
shapes := []Shape{
Rectangle{Width: 3, Height: 4},
Circle{Radius: 5},
}
fmt.Printf("aire totale : %.2f\n", totalArea(shapes))
}
Son exécution affiche :
aire totale : 90.54
L’interface vide et any
Une interface sans méthode est satisfaite par tous les types. Elle s’écrit
interface{}, mais depuis Go 1.18 l’alias intégré any a le même sens et se lit
bien mieux.
var x any = 42 // contient un int
x = "maintenant une chaîne" // contient une string
x = []int{1, 2, 3} // contient une slice
Utilisez any avec parcimonie : il jette l’information de type, donc vous
perdez la vérification à la compilation. Préférez une interface précise dès que
possible.
Les assertions de type
Pour récupérer la valeur concrète stockée dans une interface, utilisez une
assertion de type. La forme virgule-ok est sûre : une mauvaise supposition met
ok à false au lieu de provoquer une panique.
var i any = "bonjour"
s := i.(string) // s == "bonjour"
n, ok := i.(int) // ok == false, n == 0 (pas de panique)
if s2, ok := i.(string); ok {
fmt.Println("c'est une chaîne :", s2)
}
L’interface Stringer
La bibliothèque standard définit fmt.Stringer, une interface à une seule
méthode String() string. Le package fmt la recherche automatiquement :
l’implémenter contrôle donc l’affichage de votre type.
package main
import "fmt"
// Point implémente fmt.Stringer en définissant une méthode String.
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func main() {
p := Point{X: 1, Y: 2}
fmt.Println(p) // fmt utilise String() automatiquement
}
Ce programme affiche (1, 2).
Pourquoi les interfaces sont utiles
Comme la satisfaction est implicite, une fonction qui accepte une interface ne dépend d’aucun type concret. C’est ce découplage qui rend les interfaces si précieuses pour les tests : dans un test, vous passez un petit substitut qui satisfait l’interface, à la place de la vraie base de données ou du vrai client réseau.
Erreurs fréquentes
- Des interfaces trop grosses. Une interface à cinq méthodes est difficile à satisfaire et à simuler. Découpez-la, ou utilisez des interfaces à une méthode.
- Recourir à
anytrop tôt. Il désactive la vérification de type. Modélisez le comportement avec une vraie interface. - Des assertions de type qui paniquent. La forme à une valeur
i.(int)panique en cas de mauvaise supposition. Utilisez la forme virgule-ok sauf certitude. - Une interface nil qui ne l’est pas. Une interface contenant un pointeur
nil est elle-même non nil, ce qui surprend souvent lors du test
err != nil.
À vous de jouer
Définissez une interface Greeter avec une méthode Greet() string,
implémentez-la avec un struct Person, puis affichez le message via
l’interface.
Afficher la correction
package main
import "fmt"
// Greeter est satisfaite par tout type doté d'une méthode Greet.
type Greeter interface {
Greet() string
}
type Person struct {
Name string
}
// Person satisfait Greeter implicitement : aucun mot-clé « implements ».
func (p Person) Greet() string {
return "Bonjour, je m'appelle " + p.Name
}
func main() {
var g Greeter = Person{Name: "Ada"}
fmt.Println(g.Greet())
}Person ne nomme jamais Greeter, et pourtant elle la satisfait juste en ayant
la bonne méthode. La variable g a le type de l’interface : l’appel passe donc
par l’interface. Le programme affiche Bonjour, je m'appelle Ada.
Résumé
- Une interface est un ensemble de méthodes ; tout type qui les possède la satisfait.
- La satisfaction est implicite : il n’y a pas de mot-clé
implements. any(l’interface vide) contient n’importe quelle valeur ; récupérez-la avec une assertion de type.- Les petites interfaces découplent le code et facilitent les tests.
Ensuite, voyez comment l’interface error sous-tend la
gestion des erreurs en Go. Pour revoir les
types qui implémentent les interfaces, relisez les
structs et méthodes.