Ce tutoriel fait partie du parcours « Go de zéro ». Pour la vue d’ensemble, lisez d’abord le guide principal.
Structs, slices, maps et gestion des erreurs en Go
Jusqu’ici, GareBook ne connaît qu’un seul trajet, éparpillé en variables séparées. Une vraie gare en gère des dizaines, chacun avec son départ, son arrivée, son prix et ses places. Pour cela, il faut regrouper ces informations dans une structure, les empiler dans une liste dynamique, et pouvoir retrouver un trajet précis instantanément grâce à un dictionnaire. Et quand on demande un trajet qui n’existe pas, il faut renvoyer une erreur propre — pas faire planter le programme.
Ce tutoriel est central : structs, slices, maps et erreurs forment le quotidien de tout programme Go réel. À la fin, GareBook gérera une vraie collection de trajets, avec recherche, filtrage et signalement d’erreurs à la mode Go.
🎯 Ce que vous allez apprendre
- Définir une
structpour modéliser un trajet et lui attacher des méthodes ; - Stocker et parcourir une collection avec un
slice, et comprendreappend; - Indexer et retrouver des données en temps constant avec une
map; - Renvoyer et tester des erreurs de façon idiomatique avec
error,errors.Newetfmt.Errorf.
🛠️ Ce que vous allez construire
Un petit « magasin de trajets » en mémoire : une liste de trajets que l’on remplit, que l’on parcourt pour filtrer les départs d’une ville, et que l’on interroge par identifiant avec une vraie gestion d’erreur quand l’identifiant est inconnu. C’est la couche de données de GareBook, que l’API exposera plus tard en HTTP.
Prérequis
- Avoir suivi le tutoriel sur la syntaxe : variables, fonctions, conditions, boucles ;
- Un projet
garebookfonctionnel avecgo run .; - Niveau : débutant à l’aise. Test express : si vous savez écrire une fonction qui renvoie deux valeurs, vous êtes prêt.
- ⏱️ Temps estimé : ~40 minutes.
Étape 1 — Modéliser un trajet avec une struct
Une struct regroupe plusieurs champs sous un même type. Plutôt que de trimballer quatre variables séparées, on crée un type Trajet qui les rassemble. C’est l’équivalent d’une « fiche » : un objet cohérent qu’on peut copier, passer à une fonction, ranger dans une liste. Définissons-le :
package main
import "fmt"
// Trajet décrit une liaison entre deux villes.
type Trajet struct {
ID int
Depart string
Arrivee string
PrixFCFA int
Places int
}
Les champs commencent par une majuscule, ce qui n’est pas anodin en Go : une majuscule rend l’élément exporté, c’est-à-dire visible depuis d’autres packages. On en reparlera pour le JSON. Créons maintenant un trajet et affichons-le :
t := Trajet{ID: 1, Depart: "Dakar", Arrivee: "Saint-Louis", PrixFCFA: 3600, Places: 7}
fmt.Printf("%s → %s : %d FCFA (%d places)\n", t.Depart, t.Arrivee, t.PrixFCFA, t.Places)
On accède à un champ avec le point : t.Depart. On peut aussi attacher des méthodes à une struct — des fonctions liées à ce type. Ajoutons une méthode qui dit si le trajet est complet :
// Complet indique s'il ne reste aucune place.
func (t Trajet) Complet() bool {
return t.Places == 0
}
Le (t Trajet) avant le nom s’appelle le récepteur : il indique que Complet s’applique à un Trajet, accessible via t. On l’appelle ensuite naturellement : if t.Complet() { ... }. Les méthodes rendent le code lisible, parce que la logique vit à côté de la donnée qu’elle concerne.
Quand vos données s’enrichissent, Go privilégie la composition à l’héritage : on imbrique une struct dans une autre plutôt que de créer des hiérarchies de classes. Par exemple, si plusieurs types partageaient des champs d’horodatage, on définirait un petit type Horaire{ Depart, Arrivee string } qu’on inclurait dans Trajet. Les champs de la struct imbriquée deviennent alors accessibles directement, comme s’ils appartenaient au type englobant. Cette approche garde les types plats et lisibles, sans la complexité des arbres d’héritage. Retenez le principe pour plus tard ; pour GareBook, une struct simple suffit largement à démarrer.
✅ Point d’étape — Vous avez un type
Trajet, vous en créez une instance et vous appelezt.Complet(). Créez un second trajet avecPlaces: 0et vérifiez queComplet()renvoie bientrue.
Étape 2 — Une collection avec un slice
Un trajet seul ne fait pas une gare. Le slice est la liste dynamique de Go : il grandit à la demande. On le déclare avec des crochets, et on y ajoute des éléments avec append. Construisons le catalogue de GareBook :
trajets := []Trajet{
{ID: 1, Depart: "Dakar", Arrivee: "Saint-Louis", PrixFCFA: 3600, Places: 7},
{ID: 2, Depart: "Dakar", Arrivee: "Thiès", PrixFCFA: 1500, Places: 0},
{ID: 3, Depart: "Thiès", Arrivee: "Touba", PrixFCFA: 2000, Places: 12},
}
// Ajouter un trajet en cours de route
trajets = append(trajets, Trajet{ID: 4, Depart: "Dakar", Arrivee: "Mbour", PrixFCFA: 1800, Places: 5})
append renvoie un nouveau slice — c’est pourquoi on réaffecte trajets = append(trajets, ...). Oublier cette réaffectation est l’erreur de débutant numéro un avec les slices. Parcourons maintenant la liste avec for ... range pour afficher les trajets disponibles :
for _, t := range trajets {
statut := "disponible"
if t.Complet() {
statut = "COMPLET"
}
fmt.Printf("[%d] %s → %s — %d FCFA (%s)\n", t.ID, t.Depart, t.Arrivee, t.PrixFCFA, statut)
}
Le range renvoie deux valeurs à chaque tour : l’indice et l’élément. Ici on ignore l’indice avec _ et on travaille sur t, une copie du trajet. Écrivons une fonction qui filtre les départs d’une ville donnée — typiquement « montre-moi tous les bus qui partent de Dakar » :
// departsDe renvoie les trajets partant de la ville indiquée.
func departsDe(trajets []Trajet, ville string) []Trajet {
var resultat []Trajet
for _, t := range trajets {
if t.Depart == ville {
resultat = append(resultat, t)
}
}
return resultat
}
On part d’un slice vide (var resultat []Trajet, qui vaut nil mais accepte append), on accumule les trajets correspondants, et on renvoie le tout. Simple, lisible, efficace.
Étape 3 — Retrouver vite avec une map
Chercher un trajet par son identifiant en parcourant tout le slice fonctionne, mais devient lent quand la liste grandit : il faut potentiellement tout traverser. La map résout ça : c’est un dictionnaire clé-valeur qui retrouve un élément en temps quasi constant, quelle que soit la taille. Indexons les trajets par leur ID :
// indexerParID construit une map ID → Trajet.
func indexerParID(trajets []Trajet) map[int]Trajet {
index := make(map[int]Trajet)
for _, t := range trajets {
index[t.ID] = t
}
return index
}
make(map[int]Trajet) crée une map vide dont les clés sont des entiers et les valeurs des trajets. On la remplit en parcourant le slice. Pour lire une valeur, Go offre un idiome précieux, la « virgule-ok », qui distingue « la clé existe » de « la clé est absente » :
index := indexerParID(trajets)
if t, ok := index[3]; ok {
fmt.Printf("Trouvé : %s → %s\n", t.Depart, t.Arrivee)
} else {
fmt.Println("Aucun trajet avec cet ID")
}
Sans le ok, lire une clé absente renverrait silencieusement la valeur zéro du type (un Trajet tout à zéro), qu’on confondrait avec un vrai trajet. La virgule-ok lève l’ambiguïté. Retenez-la : elle revient pour le décodage JSON et la lecture de channels.
✅ Point d’étape —
departsDe(trajets, "Dakar")renvoie trois trajets, etindex[3]retrouve le trajet Thiès → Touba. Testez aussiindex[99]avec la virgule-ok :okdoit valoirfalse.
Étape 4 — Gérer les erreurs à la mode Go
Voici le cœur culturel du langage. Go n’a pas d’exceptions : une fonction qui peut échouer renvoie une valeur de type error en dernière position. Si elle vaut nil, tout va bien ; sinon, l’erreur décrit ce qui a échoué. Transformons notre recherche par ID pour qu’elle renvoie une erreur explicite quand l’identifiant est inconnu :
import "errors"
// chercherTrajet renvoie le trajet d'ID donné, ou une erreur.
func chercherTrajet(index map[int]Trajet, id int) (Trajet, error) {
t, ok := index[id]
if !ok {
return Trajet{}, fmt.Errorf("trajet %d introuvable", id)
}
return t, nil
}
La fonction renvoie (Trajet, error). En cas d’échec, on renvoie un trajet vide et une erreur construite avec fmt.Errorf, qui formate un message comme Printf. Côté appelant, on teste systématiquement l’erreur juste après l’appel — c’est le rituel central de Go :
t, err := chercherTrajet(index, 99)
if err != nil {
fmt.Println("Erreur :", err)
return
}
fmt.Printf("Réservation possible sur %s → %s\n", t.Depart, t.Arrivee)
Pour des erreurs prévisibles qu’on veut pouvoir reconnaître précisément, on déclare une erreur sentinelle avec errors.New, puis on la compare avec errors.Is. C’est utile quand l’appelant doit réagir différemment selon le type d’échec :
var ErrComplet = errors.New("trajet complet")
// reserverPlace retire une place ou renvoie ErrComplet.
func reserverPlace(t *Trajet) error {
if t.Places <= 0 {
return ErrComplet
}
t.Places--
return nil
}
// Côté appelant :
// if errors.Is(err, ErrComplet) { proposer un autre trajet }
Notez le récepteur *Trajet (un pointeur) : il permet à la méthode de modifier le trajet d’origine en décrémentant ses places. Avec un récepteur valeur, on ne modifierait qu’une copie. Cette distinction pointeur/valeur est subtile au début ; la règle pratique : si la fonction doit modifier la donnée, passez un pointeur.
Une dernière technique mérite d’être connue : l’enveloppement d’erreurs. Quand une fonction de bas niveau échoue, on veut souvent ajouter du contexte avant de remonter l’erreur, sans perdre l’erreur d’origine. Le verbe %w de fmt.Errorf fait exactement ça : fmt.Errorf("réservation impossible : %w", err) crée une nouvelle erreur lisible tout en conservant l’erreur sous-jacente, qu’on pourra retrouver plus loin avec errors.Is ou errors.As. C’est ainsi qu’on construit des messages d’erreur qui racontent toute la chaîne — « réservation impossible : trajet 99 introuvable » — au lieu d’un laconique « introuvable » sans contexte. Pour une API que vous déboguerez à distance, cette traçabilité vaut de l’or.
Étape 5 — Vérification finale
Assemblez le tout : le type Trajet avec sa méthode Complet, le slice de trajets, la fonction departsDe, l’index par map, et chercherTrajet avec sa gestion d’erreur. Dans main, affichez les départs de Dakar, cherchez un trajet existant puis un inexistant, et constatez que le programme gère élégamment les deux cas sans jamais planter. Lancez go run . : aucune panique, juste des messages clairs. C’est la marque d’un code Go bien écrit.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Les ajouts à un slice « disparaissent » | Vous appelez append sans réaffecter le résultat |
Toujours écrire s = append(s, x) |
panic: assignment to entry in nil map |
Vous écrivez dans une map non initialisée | Créez-la d’abord avec make(map[K]V) |
| Une clé absente renvoie un objet « vide » trompeur | Lecture de map sans la virgule-ok | Utilisez v, ok := m[k] et testez ok |
| Une méthode ne modifie pas la struct | Récepteur valeur : la méthode travaille sur une copie | Utilisez un récepteur pointeur (t *Trajet) pour modifier l’original |
| L’erreur renvoyée est ignorée | Réflexe des langages à exceptions | Testez if err != nil après chaque appel faillible |
🌍 Adaptation au contexte ouest-africain
Stocker des données « en mémoire » comme ici est parfait pour apprendre et pour un prototype, mais ces trajets disparaissent dès que le programme s’arrête. Pour une application réelle qui doit survivre à un redémarrage — fréquent quand l’électricité saute —, vous brancherez plus tard une base de données. La bonne nouvelle : la structure qu’on vient de bâtir (un type Trajet, des fonctions de recherche claires) se reliera à une base sans tout réécrire. C’est l’intérêt de bien séparer la donnée de son stockage dès le départ.
En attendant, garder un petit jeu de données en mémoire permet de développer et tester hors-ligne, sans serveur de base de données à installer. Idéal quand on code depuis un endroit à connexion intermittente.
✅ Récapitulatif
Vous maîtrisez désormais les quatre piliers de la manipulation de données en Go. La struct modélise un trajet et porte ses méthodes ; le slice stocke une collection qui grandit avec append ; la map retrouve un élément par clé en un éclair grâce à la virgule-ok ; et la gestion d’erreurs par valeur de retour — fmt.Errorf, errors.New, errors.Is — remplace les exceptions par quelque chose d’explicite et de prévisible. GareBook a maintenant une vraie couche de données. Prochaine étape : la rendre rapide grâce à la concurrence.
🧾 Aide-mémoire
| Élément | Rôle / syntaxe |
|---|---|
type T struct { ... } |
Définit une structure de données |
func (t T) M() |
Méthode à récepteur valeur (lecture) |
func (t *T) M() |
Méthode à récepteur pointeur (modification) |
[]T{...} |
Slice littéral |
s = append(s, x) |
Ajoute un élément (réaffecter !) |
make(map[K]V) |
Crée une map vide |
v, ok := m[k] |
Lecture sûre d’une map (virgule-ok) |
fmt.Errorf("...", a) |
Crée une erreur formatée |
errors.Is(err, Cible) |
Teste si une erreur correspond à une sentinelle |
💪 À vous de jouer
Ajoutez une fonction placesTotales(trajets []Trajet) int qui additionne les places disponibles de tous les trajets, et une fonction moinsCher(trajets []Trajet) (Trajet, error) qui renvoie le trajet le moins cher, ou une erreur si la liste est vide.
Voir une solution
func placesTotales(trajets []Trajet) int {
total := 0
for _, t := range trajets {
total += t.Places
}
return total
}
func moinsCher(trajets []Trajet) (Trajet, error) {
if len(trajets) == 0 {
return Trajet{}, errors.New("aucun trajet disponible")
}
meilleur := trajets[0]
for _, t := range trajets[1:] {
if t.PrixFCFA < meilleur.PrixFCFA {
meilleur = t
}
}
return meilleur, nil
}
len(trajets) donne la taille du slice ; trajets[1:] est une « tranche » à partir du second élément. On garde le moins cher au fil du parcours.
Tutoriels frères
- Les bases de la syntaxe Go — le tutoriel précédent.
- La concurrence pour débuter — la suite : traiter ces données en parallèle.
Pour aller plus loin
- 🔝 Retour au guide principal : Go (Golang) : le guide complet pour débuter
- Documentation officielle des erreurs : Error handling and Go
- Prochain tutoriel : La concurrence pour débuter
FAQ
Slice ou tableau (array) ?
Un array Go a une taille fixe ([3]int) et sert rarement directement. Le slice ([]int) a une taille dynamique et c’est ce qu’on utilise dans 99 % des cas. Quand on dit « liste » en Go, on pense slice.
Quand récepteur pointeur, quand récepteur valeur ?
Récepteur pointeur (*T) si la méthode modifie la struct ou si la struct est grosse (pour éviter une copie). Récepteur valeur (T) pour de la simple lecture sur de petites structures. En cas de doute, restez cohérent au sein d’un même type.
Faut-il toujours utiliser fmt.Errorf ?
Pour un message dynamique (avec un ID, un nom), oui. Pour une erreur fixe qu’on veut comparer avec errors.Is, déclarez plutôt une sentinelle avec errors.New. Et pour « envelopper » une erreur sous-jacente, fmt.Errorf("...: %w", err) conserve l’erreur d’origine.
Une map garde-t-elle l’ordre d’insertion ?
Non. L’ordre de parcours d’une map est volontairement aléatoire en Go. Si l’ordre compte, gardez une liste (slice) des clés à côté, ou triez avant d’afficher.
Mots-clés : struct Go, slice Go, map Go, append, gestion erreurs Go, errors.Is, fmt.Errorf, méthodes Go.