backend / go
Structs et méthodes
Explication
Ce que vous allez apprendre
- Définir une struct pour regrouper des champs liés en un seul type
- Attacher des méthodes à un type via un receiver, sans mot-clé
class - Comprendre la différence cruciale entre receiver par valeur et receiver par pointeur
- Utiliser l'embedding pour composer des structs, à la place de l'héritage
- Comparer des structs et créer des structs anonymes ponctuelles
Dans quel contexte ?
Un développeur qui vient de Java ou C# ouvre un fichier Go et cherche le mot-clé class : il ne le trouve nulle part. À la place, il découvre une struct Utilisateur toute simple et une fonction func (u Utilisateur) Description() string qui semble attachée au type par un lien étrange. Comprendre ce mécanisme de "receiver" est la clé pour lire n'importe quel code Go orienté données.
D'abord, une struct n'est qu'un regroupement de champs
Contrairement à une classe, une struct Go ne contient jamais de méthode directement dans sa définition. Elle décrit uniquement la forme des données : type Utilisateur struct { Nom string; Email string; Age int }. Le comportement est ajouté séparément, via des fonctions qui déclarent un "receiver".
Cette séparation nette entre données et comportement est un choix de conception délibéré : Go préfère la composition explicite à l'héritage, qu'il ne propose d'ailleurs pas du tout.
Une fois la struct définie, comment lui attacher un comportement ?
La syntaxe func (u Utilisateur) Description() string { ... } déclare une méthode sur le type Utilisateur, où u est le receiver — l'équivalent du this/self d'autres langages, mais explicitement nommé et typé.
Il existe deux types de receiver, et le choix entre les deux change fondamentalement le comportement de la méthode.
| Type de receiver | Syntaxe | Effet sur l'appelant |
|---|---|---|
| Par valeur | func (u Utilisateur) M() | Reçoit une copie, ne peut pas modifier l'original |
| Par pointeur | func (u *Utilisateur) M() | Modifie réellement l'instance d'origine |
Prérequis
Cette leçon utilise des notions de fonctions (paramètres, retours) déjà vues précédemment. La notion de pointeur (*T, &x) sera creusée en détail dans une leçon dédiée plus loin, mais un aperçu suffit ici pour comprendre les receivers.
Il reste un piège classique lié à ce choix de receiver
Si une méthode doit modifier l'état de la struct (comme Vieillir() qui incrémente l'âge), elle doit impérativement utiliser un receiver par pointeur (*Utilisateur). Utiliser un receiver par valeur dans ce cas ne provoque aucune erreur de compilation, mais la modification n'a aucun effet visible en dehors de la méthode — un bug silencieux redoutable pour un débutant.
Piège fréquent
Mélanger des méthodes à receiver par valeur et par pointeur sur le même type fonctionne, mais complique la lecture du code : par convention, si UNE méthode d'un type a besoin d'un pointeur, toutes les méthodes de ce type devraient l'utiliser aussi, par cohérence.
Ensuite, la façon dont Go remplace l'héritage : la composition
Une struct peut "embarquer" une autre struct sans lui donner de nom de champ, comme Employe qui embarque Utilisateur et Adresse. Les champs et méthodes de la struct embarquée deviennent alors directement accessibles depuis l'extérieur, comme s'ils appartenaient à Employe — c'est ce qu'on appelle la "promotion" de champs.
Bonne pratique
Privilégie systématiquement la composition (embedding) à toute tentative de reproduire un héritage à la Java en Go : c'est l'idiome que la communauté et la bibliothèque standard utilisent partout, et il évite les hiérarchies de types rigides et fragiles.
Une struct est comparable avec == uniquement si tous ses champs le sont — une propriété pratique à connaître avant d'utiliser des structs comme clés de map. Maintenant que tu sais structurer des données concrètes, la prochaine leçon montre comment écrire du code qui fonctionne avec plusieurs types différents sans connaître leur implémentation exacte : les interfaces.
Commandes & code
Structs et méthodes
Go n'a pas de classes : des struct pour les données, des méthodes attachées via un receiver.
package main
import "fmt"
type Utilisateur struct {
Nom string
Email string
Age int
}
// Methode avec receiver par valeur : recoit une COPIE de l'utilisateur
func (u Utilisateur) Description() string {
return fmt.Sprintf("%s (%s), %d ans", u.Nom, u.Email, u.Age)
}
// Methode avec receiver par pointeur : peut MODIFIER l'original
func (u *Utilisateur) Vieillir() {
u.Age++
}
// Struct imbriquee (composition, pas d'heritage en Go)
type Adresse struct {
Rue, Ville, CodePostal string
}
type Employe struct {
Utilisateur // embedding : Employe "herite" des champs/methodes
Adresse // multiple embedding possible
Salaire float64
Poste string
}
func main() {
// Construction avec noms de champs (recommande)
u := Utilisateur{Nom: "Alice", Email: "alice@example.com", Age: 30}
// Construction positionnelle (fragile si l'ordre change, a eviter)
u2 := Utilisateur{"Bob", "bob@example.com", 25}
fmt.Println(u.Description())
fmt.Println(u2.Description())
u.Vieillir()
fmt.Println(u.Age) // 31 : la methode a modifie l'original via le pointeur
// Pointeur explicite vers une struct
pu := &Utilisateur{Nom: "Carla", Age: 40}
pu.Vieillir() // Go dereference automatiquement : equivalent a (*pu).Vieillir()
fmt.Println(pu.Age)
e := Employe{
Utilisateur: Utilisateur{Nom: "Dan", Age: 35},
Adresse: Adresse{Ville: "Lyon"},
Salaire: 45000,
Poste: "Backend Dev",
}
// Acces direct aux champs/methodes embarques (promotion)
fmt.Println(e.Nom, e.Ville, e.Description())
// Comparaison de structs : possible si tous les champs sont comparables
a1 := Adresse{Rue: "1 rue A", Ville: "Paris"}
a2 := Adresse{Rue: "1 rue A", Ville: "Paris"}
fmt.Println(a1 == a2) // true
// Struct anonyme : utile pour des donnees jetables
point := struct{ X, Y int }{X: 3, Y: 4}
fmt.Println(point)
}Résumé
- Go remplace l'héritage par la composition (embedding de structs).
- Receiver par valeur = copie ; receiver par pointeur
*T= modification réelle. - Règle pratique : si une méthode modifie l'état ou que la struct est grosse, utiliser un pointeur.
- Les structs sont comparables avec
==si tous leurs champs le sont.
Exercices pratiques
Mission : des employés qui ne vieillissent jamais dans la base
Objectif : Corriger un bug de receiver par valeur, puis comprendre pourquoi le parcourir avec range peut encore piéger même avec le bon receiver.
Contexte
Un job nocturne doit incrémenter l'âge de chaque Utilisateur d'une entreprise le jour de son anniversaire. La méthode Vieillir() existe, le job tourne sans erreur chaque nuit, mais les âges en base ne changent jamais.