Les méthodes : attacher un comportement à un type
Jusqu'ici vos struct ne faisaient que stocker des données. Une méthode leur attache un comportement : c'est une fonction avec un récepteur, déclaré entre func et le nom.
type Compte struct {
solde float64
}
// Récepteur VALEUR : lecture seule (travaille sur une copie)
func (c Compte) Solde() float64 {
return c.solde
}
// Récepteur POINTEUR : peut modifier le Compte d'origine
func (c *Compte) Deposer(montant float64) {
c.solde += montant
}
func main() {
c := Compte{}
c.Deposer(50) // Go prend l'adresse automatiquement
fmt.Println(c.Solde()) // 50
}
La distinction est cruciale : un récepteur valeur (c Compte) reçoit une copie, donc il ne peut pas modifier l'original. Un récepteur pointeur (c *Compte) reçoit l'adresse, donc il peut muter le type.
Quel récepteur choisir ? Pointeur (*T) dès que la méthode doit modifier le type, ou si le type est gros (éviter la copie). Valeur (T) pour de la lecture seule sur de petits types. Règle pratique : sois cohérent, ne mélange pas récepteurs valeur et pointeur sur un même type.
Les interfaces : un contrat, pas une hiérarchie
Une interface décrit un comportement attendu : une liste de méthodes. Tout type qui possède ces méthodes satisfait l'interface, automatiquement, sans aucun mot-clé implements. C'est la grande différence avec Java ou PHP.
Imaginez une interface type Parleur interface { Parler() string } et un type Chien qui possède une méthode Parler() string. Avant de dérouler : faut-il écrire explicitement quelque part, avec un mot-clé du genre implements Parleur, que Chien satisfait l'interface ? Comment Go décide-t-il qu'un type satisfait une interface ?
Voir la réponse
Non, il n'y a aucun mot-clé implements en Go. La satisfaction d'une interface est implicite (typage structurel, souvent appelé « duck typing ») : dès qu'un type possède toutes les méthodes exigées par une interface (mêmes noms, mêmes signatures), il satisfait cette interface automatiquement, sans aucune déclaration. Chien, qui a une méthode Parler() string, est donc un Parleur sans qu'on l'écrive nulle part. Conséquence forte : le type ne « connaît » pas l'interface, et on peut définir une interface après coup pour des types existants, même ceux d'une autre bibliothèque. Les interfaces décrivent un comportement attendu, pas une hiérarchie d'héritage.
type Forme interface {
Aire() float64
}
type Rectangle struct{ L, H float64 }
func (r Rectangle) Aire() float64 { return r.L * r.H }
type Cercle struct{ R float64 }
func (c Cercle) Aire() float64 { return 3.14159 * c.R * c.R }
Rectangle et Cercle n'ont jamais déclaré « je suis une Forme ». Mais comme ils ont tous les deux une méthode Aire() float64, ils satisfont l'interface Forme. Le compilateur le vérifie tout seul.
implements : un type satisfait une interface dès qu'il en possède les méthodes.Le polymorphisme sans héritage
L'intérêt : une fonction qui prend une Forme accepte n'importe quel type qui la satisfait. Pas besoin de classe parente, pas d'arbre d'héritage.
func decrire(f Forme) {
fmt.Printf("Aire : %.2f\n", f.Aire())
}
func main() {
decrire(Rectangle{L: 3, H: 4}) // Aire : 12.00
decrire(Cercle{R: 5}) // Aire : 78.54
}
Ajoutez demain un Triangle avec une méthode Aire() : decrire fonctionnera sans changer une ligne. C'est le polymorphisme à la Go : on programme contre un comportement, pas contre une hiérarchie de types.
Le conseil idiomatique. Garde tes interfaces petites (souvent une seule méthode) et définis-les côté consommateur, pas côté producteur. La bibliothèque standard en est pleine : io.Reader, io.Writer, fmt.Stringer n'ont qu'une méthode chacune.
Révélation : error est une interface
Souvenez-vous du error de la leçon précédente. Ce n'est pas un type magique : c'est une interface, définie par la bibliothèque standard en trois lignes :
type error interface {
Error() string
}
N'importe quel type qui possède une méthode Error() string est donc un error valide. C'est ainsi qu'on crée des erreurs riches, qui portent des données :
type ErrSolde struct {
Manque float64
}
func (e ErrSolde) Error() string {
return fmt.Sprintf("solde insuffisant : il manque %.2f €", e.Manque)
}
func retirer(solde, montant float64) error {
if montant > solde {
return ErrSolde{Manque: montant - solde} // satisfait error
}
return nil
}
Le code appelant peut alors vérifier if err != nil comme d'habitude, mais aussi récupérer la donnée (le montant manquant) avec un type assertion. Les interfaces, c'est exactement ce qui rend le système d'erreurs de Go à la fois simple et extensible.
Le piège de l'interface nil qui n'est pas nil
Une variable d'interface, sous le capot, c'est une boîte à deux cases : le type concret qu'elle contient, et la valeur elle-même. Et la comparaison err == nil regarde les deux cases. C'est là que Go tend son piège le plus célèbre :
Le piège se déclenche presque toujours de la même façon : une fonction qui déclare retourner error mais renvoie un pointeur typé qui vaut nil :
func retirer(solde, montant float64) error {
var e *ErrSolde // pointeur nil pour l'instant
if montant > solde {
e = &ErrSolde{Manque: montant - solde}
}
return e // ⚠ même si e vaut nil, l'interface ne le sera pas !
}
err := retirer(100, 50)
fmt.Println(err == nil) // false… alors qu'il n'y a aucune erreur
Le remède : ne stockez jamais une erreur dans une variable pointeur avant de la retourner. Retournez nil explicitement dans le chemin sans erreur (if montant > solde { return &ErrSolde{...} } ; return nil). Le nil littéral laisse les deux cases vides ; un pointeur nil typé remplit la case type.
À toi de jouer
Complète ce programme : ajoute un type Carre avec une méthode Aire() pour qu'il satisfasse l'interface Forme et passe dans decrire. Puis exécute :
package main
import "fmt"
type Forme interface {
Aire() float64
}
type Rectangle struct{ L, H float64 }
func (r Rectangle) Aire() float64 { return r.L * r.H }
// TODO : ajoute ici un type Carre avec une méthode Aire()
func decrire(f Forme) {
fmt.Printf("Aire : %.2f\n", f.Aire())
}
func main() {
decrire(Rectangle{L: 3, H: 4})
// decrire(Carre{Cote: 5}) // décommente après avoir créé Carre
}
La solution. type Carre struct{ Cote float64 } puis func (c Carre) Aire() float64 { return c.Cote * c.Cote }. Dès que Carre a la méthode Aire() float64, il satisfait Forme : decrire(Carre{Cote: 5}) affiche Aire : 25.00, sans toucher à decrire.
Methods: attaching behavior to a type
So far your structs only stored data. A method attaches behavior to them: it's a function with a receiver, declared between func and the name.
type Account struct {
balance float64
}
// VALUE receiver: read-only (works on a copy)
func (a Account) Balance() float64 {
return a.balance
}
// POINTER receiver: can modify the original Account
func (a *Account) Deposit(amount float64) {
a.balance += amount
}
func main() {
a := Account{}
a.Deposit(50) // Go takes the address automatically
fmt.Println(a.Balance()) // 50
}
The distinction is crucial: a value receiver (a Account) gets a copy, so it can't modify the original. A pointer receiver (a *Account) gets the address, so it can mutate the type.
Which receiver? Pointer (*T) whenever the method must modify the type, or if the type is large (avoid the copy). Value (T) for read-only on small types. Rule of thumb: be consistent, don't mix value and pointer receivers on the same type.
Interfaces: a contract, not a hierarchy
An interface describes expected behavior: a list of methods. Any type that has those methods satisfies the interface — automatically, with no implements keyword. That's the big difference from Java or PHP.
Imagine an interface type Speaker interface { Speak() string } and a type Dog that has a Speak() string method. Before scrolling: do you need to write anywhere, with a keyword like implements Speaker, that Dog satisfies the interface? How does Go decide that a type satisfies an interface?
See the answer
No, there is no implements keyword in Go. Interface satisfaction is implicit (structural typing, often called "duck typing"): as soon as a type has all the methods required by an interface (same names, same signatures), it satisfies that interface automatically, with no declaration needed. Dog, which has a Speak() string method, is therefore a Speaker without writing it anywhere. Strong consequence: the type doesn't "know" the interface, and you can define an interface after the fact for existing types, even those from another package. Interfaces describe expected behavior, not an inheritance hierarchy.
type Shape interface {
Area() float64
}
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
Rectangle and Circle never declared "I am a Shape". But because both have an Area() float64 method, they satisfy the Shape interface. The compiler checks it for you.
implements keyword: a type satisfies an interface as soon as it has its methods.Polymorphism without inheritance
The payoff: a function taking a Shape accepts any type that satisfies it. No parent class, no inheritance tree.
func describe(s Shape) {
fmt.Printf("Area: %.2f\n", s.Area())
}
func main() {
describe(Rectangle{W: 3, H: 4}) // Area: 12.00
describe(Circle{R: 5}) // Area: 78.54
}
Add a Triangle tomorrow with an Area() method: describe will work without changing a line. That's Go-style polymorphism: you program against a behavior, not against a type hierarchy.
The idiomatic advice. Keep your interfaces small (often a single method) and define them on the consumer side, not the producer side. The standard library is full of them: io.Reader, io.Writer, fmt.Stringer each have just one method.
Plot twist: error is an interface
Remember error from the previous lesson. It's not a magic type: it's an interface, defined by the standard library in three lines:
type error interface {
Error() string
}
So any type with an Error() string method is a valid error. That's how you build rich errors that carry data:
type ErrBalance struct {
Missing float64
}
func (e ErrBalance) Error() string {
return fmt.Sprintf("insufficient balance: %.2f short", e.Missing)
}
func withdraw(balance, amount float64) error {
if amount > balance {
return ErrBalance{Missing: amount - balance} // satisfies error
}
return nil
}
The calling code can still check if err != nil as usual, but also recover the data (the missing amount) with a type assertion. Interfaces are exactly what makes Go's error system both simple and extensible.
The trap: the nil interface that isn't nil
Under the hood, an interface variable is a box with two slots: the concrete type it holds, and the value itself. And the err == nil comparison checks both slots. That's where Go sets its most famous trap:
The trap almost always springs the same way: a function that declares it returns error but actually returns a typed pointer that happens to be nil:
func withdraw(balance, amount float64) error {
var e *ErrBalance // nil pointer for now
if amount > balance {
e = &ErrBalance{Missing: amount - balance}
}
return e // ⚠ even if e is nil, the interface won't be!
}
err := withdraw(100, 50)
fmt.Println(err == nil) // false… even though there is no error
The cure: never park an error in a pointer variable before returning it. Return a literal nil on the no-error path (if amount > balance { return &ErrBalance{...} } ; return nil). The literal nil leaves both slots empty; a typed nil pointer fills the type slot.
Your turn
Complete this program: add a Square type with an Area() method so it satisfies the Shape interface and passes to describe. Then run it:
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
// TODO: add a Square type with an Area() method here
func describe(s Shape) {
fmt.Printf("Area: %.2f\n", s.Area())
}
func main() {
describe(Rectangle{W: 3, H: 4})
// describe(Square{Side: 5}) // uncomment after creating Square
}
The solution. type Square struct{ Side float64 } then func (s Square) Area() float64 { return s.Side * s.Side }. As soon as Square has the Area() float64 method, it satisfies Shape: describe(Square{Side: 5}) prints Area: 25.00, without touching describe.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
✨ Prompt IA
Copiez ce prompt dans Claude ou ChatGPT :
Montre-moi en Go une interface Notificateur avec une méthode Envoyer(message string) error, deux types qui la satisfont (Email et SMS), et une fonction qui envoie via n'importe quel Notificateur. Explique pourquoi il n'y a pas de mot-clé implements.
💬 Ré-explique sans regarder
L'IA te rend deux types Email et SMS qui « sont » des Notificateur. Explique avec tes mots : comment Go sait-il qu'ils satisfont l'interface, alors qu'ils ne l'ont jamais déclaré ?
implements ni héritage. Email et SMS ont chacun Envoyer(string) error, donc ils sont des Notificateur valides. On programme contre le comportement, pas contre une hiérarchie.⚖️ Juge le code de l'IA
L'IA propose ce code pour qu'un compteur s'incrémente. L'accepter ou le rejeter, et pourquoi ?
type Compteur struct{ n int }
func (c Compteur) Incr() { c.n++ }
func main() {
c := Compteur{}
c.Incr()
c.Incr()
fmt.Println(c.n) // ?
}
0, pas 2. La méthode Incr a un récepteur valeur (c Compteur) : elle travaille sur une copie, donc l'incrément est perdu à chaque appel. Pour modifier le compteur d'origine, il faut un récepteur pointeur : func (c *Compteur) Incr() { c.n++ }. C'est l'erreur n°1 sur les méthodes en Go.🧠 Rappel libre
Sans remonter : pour qu'une méthode puisse modifier le type sur lequel elle est appelée, quel type de récepteur faut-il, et pourquoi ?
func (c *T)). Un récepteur valeur (func (c T)) reçoit une copie du type : toute modification reste sur la copie et est perdue au retour. Le récepteur pointeur reçoit l'adresse de l'original, donc les modifications (c.champ = …) portent sur la vraie valeur. Go ajoute/enlève le & automatiquement à l'appel.Vos types ont un comportement et se plient à des contrats. Maintenant, écrivons du code qui marche pour plusieurs types à la fois, sans le dupliquer : les génériques.
Leçon 7 : Les génériques →