For the complete documentation index, see llms.txt. This page is also available as Markdown.

Maps (dictionnaires)

Vous pouvez trouver tout le code de ce chapitre ici

Dans tableaux et slices, vous avez vu comment stocker des valeurs de façon ordonnée. Maintenant, nous allons examiner une façon de stocker des éléments par une clé et les rechercher rapidement.

Les maps vous permettent de stocker des éléments d'une manière similaire à un dictionnaire. Vous pouvez considérer la clé comme le mot et la valeur comme la définition. Et quelle meilleure façon d'apprendre sur les Maps que de construire notre propre dictionnaire ?

Tout d'abord, en supposant que nous avons déjà quelques mots avec leurs définitions dans le dictionnaire, si nous recherchons un mot, il devrait retourner sa définition.

Écrivez le test d'abord

Dans dictionary_test.go

package main

import "testing"

func TestRecherche(t *testing.T) {
	dictionnaire := Dictionnaire{"test": "ceci est juste un test"}

	resultat := dictionnaire.Recherche("test")
	attendu := "ceci est juste un test"

	if resultat != attendu {
		t.Errorf("resultat '%s' attendu '%s'", resultat, attendu)
	}
}

Déclaration du type la plus simple ici, un map[string]string. Les maps vous permettent de stocker des éléments de façon similaire à un dictionnaire, où vous pouvez faire des recherches basées sur une clé pour obtenir une valeur.

Les clés et les valeurs peuvent être de n'importe quel type. Vous pourriez par exemple faire un map[int]string si vous vouliez rechercher des chaînes par entier ou map[string]int si vous vouliez rechercher des entiers par chaîne.

Vous pourriez faire map[string]Utilisateur pour rechercher des objets Utilisateur par nom, par exemple. La seule exigence est que la clé soit comparable. Consultez le langage Go spec pour en savoir plus.

Ce n'est peut-être pas immédiatement évident à première vue, mais nous stockons une variable de type Dictionnaire (notre map[string]string), puis nous essayons d'appeler une méthode Recherche dessus.

Nous avons déjà vu ce genre de choses dans le chapitre interfaces structurées et méthodes, mais cette fois nous allons utiliser un alias de type. Un alias de type vous permet de créer un nouveau type à partir d'un type existant.

La syntaxe est type MonType ExistantType.

Pourquoi utiliser un alias de type plutôt qu'une structure ? Un map est en soi un type de référence. Qu'est-ce que cela signifie ? Si vous passez un map à une fonction ou à une méthode et que vous le modifiez, le changement sera reflété dans le appelant. D'un autre côté, un type comme int est un type de valeur. Lorsque vous passez une valeur à une fonction ou une méthode, Go fait une copie de cette valeur.

L'aliasing de notre map avec un nouveau type spécifique nous permet d'enrichir notre type en y attachant des méthodes.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

Dans dictionnaire.go

Notre test échoue maintenant avec une sortie beaucoup plus claire.

Écrivez assez de code pour le faire passer

Accéder à un map est identique à l'accès à un tableau, c'est par le biais d'une notation entre crochets.

La différence essentielle est qu'avec un tableau vous pouvez uniquement passer des entiers comme clés, tandis qu'avec les maps vous pouvez avoir d'autres types comme les chaînes.

Un autre aspect différent est que vous pouvez passer une clé qui n'existe pas. Dans le cas des tableaux, vous obtiendrez une compilation impossible si vous essayez d'écrire un élément de tableau qui est hors limites, mais dans un map vous obtiendrez simplement une valeur zéro de ce type. Par exemple, si vous avez un map[string]int et tentez d'obtenir une clé qui n'existe pas, vous obtiendrez 0.

Cette propriété nous permet de simplement retourner le résultat d'accès à la map directement et que le test passe.

Refactoriser

Il n'y a pas grand-chose à refactoriser dans notre implémentation, mais nous pourrions apporter quelques ajustements à notre test. Une chose que nous pourrions faire est de créer une fonction d'aide assertChainesEgales pour faciliter l'écriture de tests futurs.

Nous avons maintenant une méthode Recherche très simple. Cependant, nous ne savons pas vraiment ce qui arrive lorsque l'on recherche une clé qui n'existe pas. Nous allons écrire un nouveau test pour cela.

Écrivez le test d'abord

L'utilisation de sous-tests nous aide à construire un test complet pour notre fonction Recherche.

Dans le sous-test mot connu, nous continuons à vérifier que l'on peut récupérer une définition pour un mot dans le dictionnaire.

Cependant, dans le sous-test mot inconnu, nous vérifions deux choses :

  1. Recherche retourne une erreur

  2. L'erreur contient un message indiquant pourquoi la recherche a échoué

En adaptant nos tests, nous avons également modifié notre fonction Recherche pour retourner une deuxième valeur, une erreur. En Go, la façon idiomatique de gérer cette situation est de retourner la valeur zéro du type (une chaîne vide dans ce cas) et une erreur avec un message descriptif.

Cette façon permet à l'appelant de vérifier si une erreur s'est produite et de décider quoi faire à partir de là. Dans notre cas, nous voulons renvoyer un dictionnaire sans aucune sortie d'erreur si tout va bien, ou la valeur zéro et une erreur si la clé n'est pas présente dans le dictionnaire.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

C'est la première fois que nous retournons plusieurs valeurs dans notre code. Nous pouvons le faire simplement en ajoutant des parenthèses autour des valeurs de retour.

(valeur1, valeur2)

Maintenant, nous devons créer une erreur à retourner lorsque la clé n'est pas trouvée. Nous utiliserons un moyen courant en Go pour créer des erreurs personnalisées en utilisant errors.New.

Écrivez assez de code pour le faire passer

Pour améliorer notre gestion d'erreur, nous utilisons une fonctionnalité intéressante de Go en prenant une "deuxième" valeur de retour lorsque nous accédons au map.

La deuxième valeur est un booléen qui indique si la clé a été trouvée ou non.

Cette opération est à peu près la même que

Cependant, ce qui est problématique avec cette approche, c'est que si la valeur (dans ce cas, la définition) était vide, nous ne saurions pas si c'était vraiment stocké comme vide ou si la clé n'existait pas dans le map.

En utilisant la syntaxe value, ok := map[key], nous obtenons la valeur (qui sera la valeur zéro si la clé n'est pas présente) et un booléen qui nous indique si la clé a été trouvée.

Cela nous permet de distinguer entre une clé qui n'existe pas (ok sera faux) et une clé qui est présente avec une valeur vide (ok sera vrai).

Refactoriser

Notre code semble bon, mais nous pourrions améliorer la façon dont nous gérons les erreurs. Il est généralement bon de créer des erreurs constantes que vous pouvez utiliser pour comparer plus facilement avec == qu'avec une chaîne comme err.Error().

Et maintenant, nous pouvons refactoriser notre test pour utiliser cette constante d'erreur.

Nous avons créé une fonction d'aide assertErreur pour que nos tests restent légers.

Écrivez le test d'abord

Nous avons un excellent moyen de rechercher dans le dictionnaire. Il est maintenant temps de donner à notre dictionnaire la possibilité d'ajouter de nouveaux mots.

Notre test crée un dictionnaire vide et s'assure qu'un mot est ajouté.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

Dans dictionnaire.go

Notre test échoue parce que nous ne changeons pas les données.

Écrivez assez de code pour le faire passer

Ajouter à un map est presque comme accéder à un élément, vous utilisez simplement = pour affecter une valeur à la clé.

Refactoriser

Notre fonction Ajouter est très simple et notre test passe. Mais notre test utilise des vérifications multiples qui rendent le test un peu verbeux. Nous pouvons le simplifier en créant une nouvelle fonction d'aide assertDefinition.

Notre test est maintenant plus compact et la méthode Ajouter est simple. Cependant, qu'advient-il si on redéfinit un mot ? Notre fonction actuelle permettrait à un utilisateur d'écraser une définition. Cette fonctionnalité peut être acceptable dans certains cas, mais pour ce cas, nous allons dire qu'un mot n'est ajouté que s'il n'existe pas déjà.

Écrivez le test d'abord

Pour ce test, nous avons modifié notre fonction Ajouter pour renvoyer une erreur, qui est la valeur nil (nil étant l'équivalent de null en Go) pour un scénario réussi.

Nous avons également créé un dictionnaire avec un mot afin de tester ce qui se passe si nous essayons d'ajouter un mot qui existe déjà.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

Nous avons besoin de définir notre nouvelle erreur constante et modifier notre fonction Ajouter pour retourner des erreurs.

Maintenant, nous avons besoin de vérifier si le mot existe déjà.

Écrivez assez de code pour le faire passer

Ici, nous utilisons à nouveau la deuxième valeur de retour de la recherche de map pour vérifier si le mot existe déjà avant de l'ajouter.

Refactoriser

Notre implémentation est bonne et les tests passent. Mais il y a une petite optimisation que nous pouvons faire. Nous avons déjà une fonction Recherche qui renvoie une erreur si le mot existe. Nous pouvons l'utiliser dans notre fonction Ajouter pour vérifier si le mot existe ou non.

Ici, nous sommes utilisant une instruction switch pour gérer les résultats potentiels de notre appel à Recherche. Remarquez que nous avons aussi géré le cas où Recherche pourrait retourner une erreur différente. Bien que nous ne prévoyons pas d'autres erreurs de Recherche actuellement, en gérant ce cas, nous rendons notre code plus robuste aux évolutions futures.

Écrivez le test d'abord

Maintenant, il est temps d'ajouter une fonctionnalité de mise à jour pour notre dictionnaire. Par "mise à jour", nous voulons dire changer la définition d'un mot.

La fonction MettreAJour est très similaire à Ajouter, à part que cette fois nous attendons que la fonctionnalité remplace l'ancienne définition d'un mot par une nouvelle.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

Nous ajoutons simplement une nouvelle fonction MettreAJour à notre fichier dictionnaire.go.

Maintenant, nous voyons notre test échouer :

Écrivez assez de code pour le faire passer

Notre test passe, mais l'implémentation est la même que notre fonction Ajouter. Ce n'est pas idéal, car nous savons que la fonction Ajouter échouera si le mot existe déjà, ce qui signifie que nous n'utilisons pas correctement notre fonctionnalité MettreAJour.

Refactoriser

Notre fonction MettreAJour fonctionne, mais la façon dont nous l'avons implémentée n'est pas idéale. Nous devrions vérifier si le mot existe d'abord, et si ce n'est pas le cas, retourner une erreur.

Nous avons maintenant deux sous-tests, l'un qui vérifie que nous pouvons mettre à jour un mot existant, et l'autre qui vérifie que MettreAJour renvoie une erreur s'il est appelé avec un mot qui n'existe pas dans le dictionnaire.

Essayez d'exécuter le test

Nous recevons une erreur de compilation car notre fonction MettreAJour ne retourne pas de valeur. Nous devons la mettre à jour pour qu'elle le fasse.

Maintenant, il s'exécute, mais nous avons un échec de test :

Écrivez assez de code pour le faire passer

Nous avons maintenant une implémentation correcte de MettreAJour. Encore une fois, nous avons utilisé une instruction switch pour gérer les cas où Recherche pourrait retourner différents types d'erreurs.

Écrivez le test d'abord

Notre dictionnaire est presque complet. Nous avons Recherche, Ajouter et MettreAJour. Il ne reste plus qu'à implémenter Supprimer.

Notre test vérifie que la clé a été supprimée en s'assurant que Recherche renvoie notre erreur pour un mot inexistant.

Essayez d'exécuter le test

Écrivez la quantité minimale de code pour que le test s'exécute et vérifiez la sortie du test qui échoue

Le test échoue maintenant parce que le mot n'a pas été supprimé :

Écrivez assez de code pour le faire passer

Go a une fonction intégrée delete qui fonctionne sur les maps. Elle prend un map et une clé en entrée, et supprime l'élément correspondant.

Conclusion

Dans ce chapitre, nous avons abordé beaucoup de concepts importants liés aux maps en Go.

Récapitulons ce que nous avons appris :

  1. Création d'un type basé sur un map avec un alias de type

  2. Comment travailler avec maps, y compris l'accès, l'ajout, la mise à jour et la suppression d'éléments

  3. Comment gérer les erreurs lors de l'interaction avec des maps

  4. Comment utiliser les méthodes avec notre type d'alias pour créer un type plus riche

  5. Comment utiliser des constantes pour améliorer la gestion des erreurs

Nos fonctions finales sont :

Maintenant, notre Dictionnaire est complètement fonctionnel et offre des méthodes idiomatiques qui permettent d'interagir avec le dictionnaire de manière claire et sans ambiguïté.

Mis à jour