Retour au cours

backend / go

Interfaces

Leçon 61 exercice

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 Stringer et 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.

ConceptExempleUsage
Interface classiquetype Forme interface { Aire() float64 }Regrouper un comportement métier
Interface videany (alias de interface{})Accepter absolument n'importe quel type
Interface à une méthodetype 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.

go
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 implements requis.
  • Préférer des interfaces petites (1-2 méthodes) — "accept interfaces, return structs".
  • any (alias de interface{}) accepte tout type, à utiliser avec parcimonie.
  • Type assertion v.(T) et type switch v.(type) extraient le type concret.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →