1. Deux motifs pour map : set et compteur
Quand vous voyez pour la premiere fois map, on a envie de l'utiliser au sens litteral : « cle → valeur ». Mais dans le code reel, map sert souvent de mecanisme universel de suivi : il sait repondre rapidement aux questions « ai-je deja vu cela ? » et « combien de fois cela est-il apparu ? ». C'est pour cela qu'en Go, on construit avec les maps une foule de choses qui, dans d'autres langages, sont isolees dans des structures separees.
Imaginez que vous ecriviez un petit utilitaire en ligne de commande : l'utilisateur saisit des mots/tags, et le programme doit (a) afficher la liste des valeurs uniques et (b) compter les frequences. On peut, bien sur, tout stocker dans un slice et chercher chaque fois lineairement la correspondance, mais alors le programme commencera a ralentir des centaines d'elements. map resout les deux problemes avec elegance, et aujourd'hui nous allons apprendre a lire et a ecrire ce genre de code avec assurance.
Pour vous repere, voici un mini tableau de ce que nous voulons obtenir :
| Tache | Ce qu'il faut stocker | A quoi ressemble le type en Go | Comment le verifier |
|---|---|---|---|
| Ensemble de valeurs uniques (set) | presence ou absence | |
|
| Frequences/compteurs | nombre de repetitions | |
ou |
2. Un ensemble : map[T]struct{}
Si vous venez d'un monde de langages qui proposent deja un Set, alors Go peut d'abord sembler un peu « radin » : pourquoi devrais-je tout refaire moi-meme ? En realite, Go n'est pas radin, mais minimaliste : la map de base est si pratique qu'elle permet d'obtenir un excellent set sans magie et sans nouvelles regles. Vous obtenez une structure de donnees que vous savez deja creer, parcourir et nettoyer.
L'idee d'un ensemble est simple : nous n'avons pas besoin de stocker une « valeur », nous avons seulement besoin d'un fait : « l'element est present ». Donc, comme valeur (V), on peut utiliser un type qui ne stocke rien. Un tel type existe en Go : une structure vide struct{}. Elle n'a aucun champ, et on l'utilise souvent comme marqueur, juste pour le principe.
Exemple minimal d'un ensemble
package main
import "fmt"
func main() {
set := make(map[string]struct{})
set["go"] = struct{}{}
set["map"] = struct{}{}
fmt.Println(len(set)) // 2
}
Ici, struct{} est une « valeur vide ». Nous la mettons seulement parce que map exige une valeur, mais le sens est porte par la cle.
Operations sur un ensemble : add/has/remove/size
Quand vous voyez map[string]struct{} dans le code de quelqu'un d'autre, n'essayez pas de chercher « ce qui y est stocke ». On y stocke « rien », et c'est justement tout le but. Pensez-y comme a une table « present/absent ». Les operations sur un ensemble se lisent alors presque comme en anglais : « ajouter », « verifier », « supprimer », « compter ».
Voici un ensemble compact d'operations que vous allez ecrire en permanence :
package main
import "fmt"
func main() {
tags := make(map[string]struct{})
tags["urgent"] = struct{}{} // add
_, ok := tags["urgent"] // has
delete(tags, "urgent") // remove
fmt.Println(ok, len(tags)) // true 0
}
Remarquez que delete(tags, "urgent") est sans danger meme si la cle n'existe pas — il ne fera tout simplement rien. C'est tres « zen » : moins de verifications, moins d'endroits ou l'on peut se tromper.
Pourquoi pas map[string]bool
Question logique d'un debutant : « Pourquoi ne pas faire un set comme map[string]bool ? » On peut bien stocker true/false. C'est possible. Mais c'est moins explicite et parfois plus risqué a lire. Le probleme principal : false est une valeur ordinaire, et il se distingue mal de la situation « la cle est absente ».
Regardez l'exemple : il compile, mais logiquement il peut vous embrouiller au bout d'une semaine (ou meme au bout de deux heures).
package main
import "fmt"
func main() {
m := map[string]bool{"featureA": false}
fmt.Println(m["featureA"]) // false (key exists, value false)
fmt.Println(m["featureB"]) // false (key missing, zero value)
}
Les deux fois, on affiche false, mais le sens est different. Vous pouvez bien sur utiliser l'idiome ok ici aussi, mais alors une question se pose : pourquoi utiliser bool tout court ? Si nous construisons un set, alors map[string]struct{} dit clairement au lecteur : « je suis un ensemble », et reduit le risque que quelqu'un ecrive if m[x] { ... } en confondant par erreur « pas de cle » et « cle presente mais false ».
3. Compteurs et frequences : map[string]int
Apres l'ensemble, le deuxieme motif le plus utile avec map est le compteur. Il est si frequent qu'on le retrouve dans les problemes d'algorithmique, le traitement des logs, l'analytique, l'import de donnees, et meme dans les plus modestes programmes CLI. Et ici, ce que nous savons deja nous aide : si la cle n'existe pas, la lecture renvoie la zero value.
Pour int, la zero value est 0. Donc l'ecriture counts[word]++ fonctionne meme si word apparait pour la premiere fois : on prend d'abord 0, puis on ajoute 1, puis on remet le resultat dans la map.
Mini exemple de frequences
package main
import "fmt"
func main() {
words := []string{"go", "map", "go"}
counts := make(map[string]int)
for _, w := range words {
counts[w]++
}
fmt.Println(counts["go"]) // 2
fmt.Println(counts["java"]) // 0
}
Et ici, il ne faut pas en tirer une mauvaise conclusion : 0 ne veut pas dire « la cle etait forcement presente ». Cela signifie « soit la cle n'existait pas, soit sa valeur vaut 0 ». Si vous devez distinguer ces cas, on revient a l'idiome ok.
Quand l'idiome ok est vraiment utile
Dans un compteur simple, 0 est presque toujours acceptable. Mais il existe des situations ou 0 est une vraie valeur. Par exemple, vous stockez un « solde actuel de bonus », et il peut devenir nul apres une deduction. Ou vous stockez un « nombre d'essais disponibles », et 0 signifie « la cle est presente, mais il n'y a plus d'essais ». Dans ces cas, il est utile de savoir distinguer l'absence de cle.
Petit exemple avec l'idiome ok pour map[string]int :
package main
import "fmt"
func main() {
counts := map[string]int{"go": 0}
v1, ok1 := counts["go"]
v2, ok2 := counts["java"]
fmt.Println(v1, ok1) // 0 true
fmt.Println(v2, ok2) // 0 false
}
Cette verification aide particulierement lorsque vous ecrivez du code du type « supprimer la cle si la valeur est devenue 0 ». Ce n'est pas une regle obligatoire, mais cela rend parfois les donnees plus propres : moins de cles avec des valeurs nulles.
4. Exemple : mini utilitaire TagStats
Maintenant, reunissons tout dans un petit programme en ligne de commande. Il sera simple (nous ne savons pas encore lire des lignes entieres avec des espaces de maniere pratique, donc nous lisons des « tokens »), mais tres honnete : l'utilisateur saisit n, puis n tags. Le programme affiche le nombre de tags uniques et les frequences de chaque tag.
Lire l'entree dans []string
Commencons par lire l'entree dans []string. Notez que nous reservons a l'avance la capacite du slice — vous l'avez deja fait dans la partie sur make et append.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
tags := make([]string, 0, n)
for i := 0; i < n; i++ {
var t string
fmt.Scan(&t)
tags = append(tags, t)
}
fmt.Println(tags) // example: [go map go]
}
Construire un set et un compteur a partir des memes donnees
Construisons maintenant simultanement un ensemble de tags uniques et un compteur :
unique := make(map[string]struct{})
counts := make(map[string]int)
for _, t := range tags {
unique[t] = struct{}{}
counts[t]++
}
fmt.Println("unique:", len(unique)) // unique: 2
Pour l'instant, nous n'avons affiche que le nombre d'elements uniques. Mais on a aussi envie d'afficher les frequences.
Sortie stable : trier les cles
L'ordre des maps n'est pas garanti, donc pour obtenir un resultat propre et reproductible, nous rassemblons les cles et les trions.
package main
import (
"fmt"
"sort"
)
func main() {
counts := map[string]int{"go": 2, "map": 1}
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, counts[k])
}
// go 2
// map 1
}
Si l'on assemble tout, on obtient une « petite application » que l'on pourra vraiment etendre dans les themes suivants (par exemple, stocker des « taches » et des « tags de taches »). Pour l'instant, sans structures, nous sommes limites dans la modelisation des donnees, mais nous pouvons deja faire une analytique utile sur des chaines.
Schema du flux de donnees
Petit schema du flux de donnees (pour que le cerveau ne fasse pas semblant d'etre une nil-map) :
flowchart TD
A[Read n tags] --> B[For each tag t]
B --> C["unique[t] = struct{}{}"]
B --> D["counts[t]++"]
C --> E["Unique = len(unique)"]
D --> F[Keys counts -> sort -> print]
5. Limites des cles : ce que l'on peut mettre dans map et set
On a parfois l'impression que map est un sac magique dans lequel on peut jeter n'importe quelles cles : des chaines, des nombres, n'importe quoi. Mais une map a une contrainte importante : la cle doit pouvoir etre comparee et « hachee ». En langage humain, cela signifie que la cle doit etre comparable (supporter ==/!= sans surprise). Par consequent, par exemple, les slices ne peuvent pas servir de cles.
Il existe meme des scenarii ou une cle « en apparence » accepte plusieurs types, et dans ce cas, essayer d'utiliser un type non comparable peut provoquer un crash avec un message du genre "hash of unhashable type []int". Dans le cadre de notre cours, retenez simplement une regle facile : pour les sets et les compteurs de la lecon d'aujourd'hui, prenez string ou int comme cles — et vous vivrez tranquillement, sans paniques inattendues.
6. Erreurs typiques
Erreur n°1 : faire un set sous forme de map[string]bool et le verifier avec if set[x] { ... }.
Un tel code « fonctionne » parfois, mais il se lit mal et se casse facilement si quelqu'un commence a stocker false comme valeur significative. Si vous voulez vraiment un ensemble, map[string]struct{} exprime mieux l'intention : la presence de l'element se verifie via _, ok := set[x], et cela ne se confond pas avec les valeurs.
Erreur n°2 : essayer de verifier l'absence d'une cle dans un compteur avec if counts[k] == 0.
Pour les frequences, c'est souvent acceptable, mais logiquement cela signifie non pas « la cle est absente », mais « la valeur actuelle vaut 0 ». Si vous devez distinguer « jamais presente » et « presente mais devenue 0 », utilisez v, ok := counts[k]. Cette regle sauve aussi dans les situations ou vous voulez supprimer les cles a zero sans savoir si elles etaient vraiment presentes.
Erreur n°3 : oublier make et ecrire dans une nil-map.
var set map[string]struct{} semble normal, et la lecture depuis une telle map ne plantera pas. Mais des que vous faites set["x"] = struct{}{}, le programme s'effondre a l'execution. Donc la regle pratique est simple : si vous comptez ajouter des elements, creez la map avec make(...) avant la premiere ecriture.
Erreur n°4 : attendre un « bel ordre » lors de l'affichage d'un compteur avec for k, v := range counts.
L'ordre de parcours d'une map n'est pas contractuel, il peut changer. A cause de cela, les tests, la comparaison des sorties et meme la simple « verification a l'oeil » deviennent penibles. Si vous affichez les resultats a l'utilisateur et que vous voulez de la stabilite, utilisez la recette deja connue : reunir les cles dans un slice, trier, puis afficher selon les cles.
Erreur n°5 : stocker dans le set autre chose que ce que vous verifiez.
Cela parait amusant, mais cela arrive souvent : vous ajoutez dans le set des chaines dans un format, par exemple avec des casses differentes : "Go" et "go", puis vous verifiez dans un autre format, et « pour une raison mysterieuse » rien n'est trouve. Si vous construisez un ensemble de chaines, mettez-les d'abord dans une forme unique (au moins avec strings.ToLower), et seulement ensuite ajoutez-les dans map[string]struct{}.
GO TO FULL VERSION