Ci-dessous, les différences entre deux révisions de la page.
Prochaine révision | Révision précédente | ||
dev:go:tutoriels:ecrire_un_module_en_go [2023/08/02 13:41] – créée yoann | dev:go:tutoriels:ecrire_un_module_en_go [2023/08/06 10:13] (Version actuelle) – yoann | ||
---|---|---|---|
Ligne 1: | Ligne 1: | ||
{{tag> | {{tag> | ||
- | |||
- | :TODO: | ||
====== Écrire un module en Go ====== | ====== Écrire un module en Go ====== | ||
+ | |||
+ | Ce tutoriel propose de créer **deux modules**: | ||
+ | * Le premier sera une bibliothèque prévue pour être importée par d' | ||
+ | * Le second est une application appelante utilisant le premier. | ||
+ | |||
+ | |||
+ | Dans ce tutoriel on va : | ||
+ | - Créer un module -- Implémenter un petit module contenant des fonctions qui pourront être appelées depuis un autre module ; | ||
+ | - Appeler le code depuis un autre module -- Importer et utiliser notre nouveau module ; | ||
+ | - Retourner et contrôler une erreur -- Ajouter un contrôle élémentaire des erreurs ; | ||
+ | - Retourner une valeur quelconque -- Manipuler des données dans des slices (tableaux à taille dynamique en Go) ; | ||
+ | - Retourner des valeurs pour l' | ||
+ | - Ajouter des test -- Utiliser les fonctionnalités intégrés de Go pour les test unitaires du code ; | ||
+ | - Compilation et installation de l' | ||
+ | |||
+ | ===== Un module utilisable par les autres ===== | ||
+ | |||
+ | En Go, le module est l' | ||
+ | |||
+ | Le code en Go est regroupé en paquetages et les paquetages sont groupés et distribués dans des modules. Le module déclare les dépendances nécessaires pour l' | ||
+ | |||
+ | Lorsqu' | ||
+ | |||
+ | Déclarer votre nouveau module en utilisant la commande **'' | ||
+ | |||
+ | <code bash> | ||
+ | # créer un repertoire pour le code | ||
+ | mkdir greetings && cd greetings | ||
+ | |||
+ | # initialise le module | ||
+ | go mod init example.com/ | ||
+ | </ | ||
+ | |||
+ | La commande **go mod init** crée le fichier go.mod qui permet le suivi des dépendances. Pour le moment le fichier ne contient que le nom du module et la version de Go supportée. | ||
+ | |||
+ | <code bash> | ||
+ | cat go.mod | ||
+ | module example.com/ | ||
+ | |||
+ | go 1.20 | ||
+ | </ | ||
+ | |||
+ | Lorsque des dépendances seront ajoutées, le fichier go.mod indiquera les versions des modules desquelles dépend votre code. Cela permet de garder la compilation reproductible et offre un contrôle direct sur les versions des modules à utiliser. | ||
+ | |||
+ | Via votre éditeur créer le fichier greetings.go avec le contenu suivant : | ||
+ | |||
+ | <code go greetings.go> | ||
+ | package greetings | ||
+ | |||
+ | import " | ||
+ | |||
+ | // Hello retourne un message de bienvenue à la personne désignée | ||
+ | func Hello( name string) string { | ||
+ | message := fmt.Sprintf(" | ||
+ | return message | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dans ce code on a: | ||
+ | * Déclarer un paquetage nommé " | ||
+ | * Définit la fonction Hello() pour retourner un message de salutation. | ||
+ | |||
+ | La fonction Hello() prend un argument nommé de type chaîne de caractères et retourne une chaîne de caractères. En Go les fonctions commençant par une majuscule peuvent être appelées en dehors du paquetage. En Go ce comportement est désigné " | ||
+ | |||
+ | L' | ||
+ | |||
+ | ===== Appel du code depuis un autre module ===== | ||
+ | |||
+ | Nous allons ici écrire le code appelant la fonction Hello() du module greetings. | ||
+ | |||
+ | Créer un répertoire hello au même niveau que greetings | ||
+ | <code bash> | ||
+ | cd .. | ||
+ | mkdir hello | ||
+ | </ | ||
+ | |||
+ | Après cette opération on a donc l' | ||
+ | < | ||
+ | . | ||
+ | ├── grettings | ||
+ | │ ├── go.mod | ||
+ | │ └── greetings.go | ||
+ | └── hello | ||
+ | </ | ||
+ | |||
+ | On entre dans le répertoire hello et on initialise le suivi du module : | ||
+ | <code bash> | ||
+ | go mod init example.com/ | ||
+ | </ | ||
+ | |||
+ | Ouvrir l' | ||
+ | <code go hello.go> | ||
+ | package main | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | |||
+ | " | ||
+ | ) | ||
+ | |||
+ | func main() { | ||
+ | msg := greetings.Hello(" | ||
+ | fmt.Println(msg) | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dans ce code : | ||
+ | * On déclare un paquetage " | ||
+ | * On importe deux paquetages : " | ||
+ | * On affiche le message via l' | ||
+ | |||
+ | |||
+ | On doit maintenant éditer le module " | ||
+ | |||
+ | Dans le cas où le module est publié les outils Go peuvent le télécharger. Ici ce n'est pas le cas : il faut donc adapter le module " | ||
+ | |||
+ | <code bash> | ||
+ | go mod edit -replace example.com/ | ||
+ | </ | ||
+ | |||
+ | La commande spécifie que " | ||
+ | |||
+ | <code bash> | ||
+ | cat go.mod | ||
+ | module example.com/ | ||
+ | |||
+ | go 1.20 | ||
+ | |||
+ | replace example.com/ | ||
+ | </ | ||
+ | |||
+ | Toujours depuis la ligne de commande dans le répertoire hello, exécuter la commande **'' | ||
+ | |||
+ | <code bash> | ||
+ | go mod tidy | ||
+ | </ | ||
+ | |||
+ | Après exécution de la commande le ficheir go.mod doit être de la forme : | ||
+ | < | ||
+ | module example.com/ | ||
+ | |||
+ | go 1.20 | ||
+ | |||
+ | replace example.com/ | ||
+ | |||
+ | require example.com/ | ||
+ | </ | ||
+ | |||
+ | La commande a bien trouvé le code dans le répertoire local ../ | ||
+ | |||
+ | LEn complément du module on retrouve une pseudo-version générée à la place de la version semantique (semantic version number). La documentation officielle décrit en détail le [[https:// | ||
+ | |||
+ | <code bash> | ||
+ | go run . | ||
+ | Salut, Yoann. Bienvenue! | ||
+ | </ | ||
+ | |||
+ | ===== Retourner et contrôler une erreur ===== | ||
+ | |||
+ | La gestion des erreurs est nécessaire à la production d' un code solide. On va maintenant aborder la génération d'une erreur depuis le module ' | ||
+ | |||
+ | |||
+ | Via l' | ||
+ | |||
+ | <code go greetings.go> | ||
+ | package greetings | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | // Hello returns a greeting for the named person. | ||
+ | func Hello(name string) (string, error) { | ||
+ | // If no name was given, return an error with a message. | ||
+ | if name == "" | ||
+ | return "", | ||
+ | } | ||
+ | |||
+ | // If a name was received, return a value that embeds the name | ||
+ | // in a greeting message. | ||
+ | message := fmt.Sprintf(" | ||
+ | return message, nil | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | A propos des modifications apportées: | ||
+ | * La fonction Hello() retourne deux valeurs de type ' | ||
+ | * On a importé le paquetage " | ||
+ | * Une instruction " | ||
+ | * Le retour normal de la fonction contient la valeur nil en seconde valeur. | ||
+ | |||
+ | On peut à présent modifier le code appelant dans le fichier '' | ||
+ | |||
+ | <code go hello.go> | ||
+ | package main | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | |||
+ | " | ||
+ | ) | ||
+ | |||
+ | func main() { | ||
+ | // | ||
+ | log.Default().SetPrefix(" | ||
+ | // | ||
+ | //de ligne | ||
+ | log.SetFlags((0)) | ||
+ | |||
+ | msg, err := greetings.Hello("" | ||
+ | |||
+ | //Si une erreur est retournée, elle est affichée dans la console | ||
+ | //et le programme s' | ||
+ | if err != nil { | ||
+ | log.Fatal(err) | ||
+ | } | ||
+ | |||
+ | //Affiche le message dans la console | ||
+ | fmt.Println(msg) | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dans le code ci-dessus on a : | ||
+ | * Configurer le paquetage " | ||
+ | * Assigné les deux valeurs retournées par Hello à des variables ; | ||
+ | * Modifié l' | ||
+ | * Testé la valeur de l' | ||
+ | * Utilisé les fonctions du paquetage " | ||
+ | |||
+ | Depuis la ligne de commande, on peut exécuter notre code: | ||
+ | |||
+ | <code bash> | ||
+ | go run . | ||
+ | greetings: empty name | ||
+ | exit status 1 | ||
+ | </ | ||
+ | |||
+ | ===== Retourner un message aléatoire ===== | ||
+ | |||
+ | On va modifier un peu le code pour retourner un message aléatoirement parmi un petit ensemble de messages. Pour cela on va utiliser une Go slice. Une slice peut être vu comme un tableau avec la particularité de pouvoir changer de taille dynamiquement lors des ajouts/ | ||
+ | |||
+ | Nous allons ajouter une slice contenant trois messages et le code permettant d'en retourner aléatoirement un parmi les trois. | ||
+ | |||
+ | On ouvre l' | ||
+ | |||
+ | <code go greetings.go> | ||
+ | package greetings | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | // Hello retourne un message de bienvenue à la personne désignée | ||
+ | func Hello(name string) (string, error) { | ||
+ | //Si aucun nom n'est donné retourne une erreur avec un message. | ||
+ | if name == "" | ||
+ | return "", | ||
+ | } | ||
+ | |||
+ | message := fmt.Sprintf(randomFormat(), | ||
+ | return message, nil | ||
+ | } | ||
+ | |||
+ | // randomFormat retourne aléatoirement un message préformatté parmi l' | ||
+ | func randomFormat() string { | ||
+ | formats := []string{ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | |||
+ | //retourne aléatoirement un format de message | ||
+ | return formats[rand.Intn(len(formats))] | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Quelques remarques: | ||
+ | * La fonction randomFormat() commence par une minuscule la rendant accessible seulement par le code de son propre paquetage (en d' | ||
+ | * La fonction randomFormat() déclare une slice '' | ||
+ | * On utilise le paquetage " | ||
+ | * Dans la fonction Hello() on appelle maintenant randomFormat() pour obtenir un message aléatoire. | ||
+ | |||
+ | |||
+ | Le fichier hello.go est légèrement modifié : on remplace la chaîne vide par une chaîne de caractères quelconque pour retrouver le comportement normal du programme. | ||
+ | |||
+ | <code bash> | ||
+ | yoann@node-7c87: | ||
+ | Hello, Yoann. Bienvenue! | ||
+ | yoann@node-7c87: | ||
+ | Heureux de vous revoir Yoann! | ||
+ | yoann@node-7c87: | ||
+ | Heureux de vous revoir Yoann! | ||
+ | yoann@node-7c87: | ||
+ | Heureux de vous revoir Yoann! | ||
+ | yoann@node-7c87: | ||
+ | Hello, Yoann. Bienvenue! | ||
+ | </ | ||
+ | |||
+ | ===== Retourner un message à plusieurs personnes ===== | ||
+ | |||
+ | Nous allons modifier une dernière fois nos modules afin d' | ||
+ | |||
+ | Ces modifications soulèvent une problématique : modifier les paramètres de la fonction Hello() change sa signature. Si le module a déjà été publié et qu'il est utilisé, ces changements conduiront à une à des erreurs dans les programmes appelants. | ||
+ | |||
+ | Dans ce cas une meilleur option est d' | ||
+ | |||
+ | Via l' | ||
+ | <code go greetings.go> | ||
+ | package greetings | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | // Hello retourne un message de bienvenue à la personne désignée | ||
+ | func Hello(name string) (string, error) { | ||
+ | //Si aucun nom n'est donné retourne une erreur avec un message. | ||
+ | if name == "" | ||
+ | return "", | ||
+ | } | ||
+ | |||
+ | message := fmt.Sprintf(randomFormat(), | ||
+ | return message, nil | ||
+ | } | ||
+ | |||
+ | // HelloAll retourne un map associant un message de bienvenue à chaque personne en entrée | ||
+ | func HelloAll(names []string) (map[string]string, | ||
+ | //un map pour associer un message à chaque nom | ||
+ | messages := make(map[string]string) | ||
+ | |||
+ | //boucle sur la slice en entrée (les noms) et appelle Hello() pour associer une message | ||
+ | for _, name := range names { | ||
+ | //pour chaque index(non utilisé _), valeur du paramètre ' | ||
+ | msg, err := Hello(name) | ||
+ | |||
+ | if err != nil { | ||
+ | //une erreur s'est produite, on retourne un objet vide et le code erreur | ||
+ | return nil, err | ||
+ | } | ||
+ | |||
+ | messages[name] = msg | ||
+ | } | ||
+ | |||
+ | return messages, nil | ||
+ | } | ||
+ | |||
+ | // randomFormat retourne aléatoirement un message préformatté parmi l' | ||
+ | func randomFormat() string { | ||
+ | formats := []string{ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | |||
+ | //retourne aléatoirement un format de message | ||
+ | return formats[rand.Intn(len(formats))] | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dans ce code nous avons: | ||
+ | * Ajouté une fonction HelloAll() ayant en paramètre une slice de plusieurs noms et retournant un map permettant d' | ||
+ | * La nouvelle fonction HelloAll() appelle la fonction Hello(). Cet usage des fonction est aussi désigné factorisation et réduit les risques d' | ||
+ | * Créé une variable " | ||
+ | * Exécuter une boucle sur chaque élément de la slice " | ||
+ | |||
+ | Dans notre fichier hello.go nous pouvons à présent faire quelques changements : | ||
+ | |||
+ | <code go hello.go> | ||
+ | package main | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | |||
+ | " | ||
+ | ) | ||
+ | |||
+ | func main() { | ||
+ | // | ||
+ | log.Default().SetPrefix(" | ||
+ | // | ||
+ | //de ligne | ||
+ | log.SetFlags((0)) | ||
+ | |||
+ | // | ||
+ | names := []string{" | ||
+ | |||
+ | //Obtenir un message de bienvenue pour chaque nom | ||
+ | messages, err := greetings.HelloAll(names) | ||
+ | |||
+ | //Si une erreur est retournée, elle est affichée dans la console | ||
+ | //et le programme s' | ||
+ | if err != nil { | ||
+ | log.Fatal(err) | ||
+ | } | ||
+ | |||
+ | //Affiche le message dans la console | ||
+ | fmt.Println(messages) | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Les changements sont les suivants : | ||
+ | * Création d'une variable " | ||
+ | * Passage en argument de la variable " | ||
+ | |||
+ | Exécutons le code ainsi modifié : | ||
+ | <code bash> | ||
+ | go run . | ||
+ | map[Alfred: | ||
+ | </ | ||
+ | |||
+ | ===== Ajout d' un test unitaire ===== | ||
+ | |||
+ | Maintenant que notre code a implémenter les fonctionnalités souhaitées, | ||
+ | |||
+ | Go intègre le support des tests unitaires. Le paquetage " | ||
+ | |||
+ | * Dans le répertoire greetings, créer un fichier '' | ||
+ | * Éditer le fichier greetings_test.go avec le contenu suivant : | ||
+ | |||
+ | <code go greetings_test.go> | ||
+ | package greetings | ||
+ | |||
+ | import ( | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | // TestHelloName appelle Hello() avec un nom et vérifie | ||
+ | // la valeur retournée. | ||
+ | func TestHelloName(t *testing.T) { | ||
+ | aName := " | ||
+ | want := regexp.MustCompile(`\b` + aName + `\b`) | ||
+ | msg, err := Hello(" | ||
+ | |||
+ | if !want.MatchString(msg) || err != nil { | ||
+ | t.Fatalf(`Hello(" | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // TestHelloEmpty appelle Helo() avec une chaine vide doit | ||
+ | // retourner une erreur. | ||
+ | func TestHelloEmpty(t *testing.T) { | ||
+ | msg, err := Hello("" | ||
+ | |||
+ | if msg != "" | ||
+ | t.Fatalf(`Hello("" | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dans ce code on a : | ||
+ | * Définit des fonctions de test dans le même paquetage que le code à tester ; | ||
+ | * Définit deux fonctions de test pour tester la fonction greetings.Hello(). Les fonctions de test doivent avoir un nom préfixé par ' | ||
+ | * A propos des deux tests: | ||
+ | * TestHelloName() appelle la fonction Hello() avec un nom en argument et vérifie que la fonction est capable de retourner un message valide. Si le retour est une erreur ou un message avec un contenu non attendu on utilise le paramètre " | ||
+ | * TestHelloEmpty() appelle la fonction Hello() avec une chaine vide. Ce test permet de valider le fonctionnement de la gestion des erreurs. Si le test retourne une chaine non vide ou ne retourne pas d' | ||
+ | |||
+ | Depuis la ligne de commande on peut lancer les tests: | ||
+ | <code powershell> | ||
+ | go test | ||
+ | </ | ||
+ | |||
+ | La commande **go test** exécute les fonctions de test (ayant des noms préfixés par " | ||
+ | |||
+ | Si on introduit (ici volontairement) une erreur lors du developpement en modifiant le comportement de la fonction Hello(), une des fonctions de test pourra révèler l' | ||
+ | |||
+ | < | ||
+ | go test -v | ||
+ | === RUN | ||
+ | greetings_test.go: | ||
+ | --- FAIL: TestHelloName (0.00s) | ||
+ | === RUN | ||
+ | --- PASS: TestHelloEmpty (0.00s) | ||
+ | FAIL | ||
+ | exit status 1 | ||
+ | FAIL example.com/ | ||
+ | </ | ||
+ | |||
+ | ===== Compiler et installer l' | ||
+ | |||
+ | Ici on aborde quelques nouvelles commandes Go. La commande go run permet de rapidement compiler et exécuter un programme lors du développement alors qu'on effectue régulièrement des modifications. | ||
+ | |||
+ | * La commande **'' | ||
+ | * La commande go **'' | ||
+ | |||
+ | Lorsque le binaire est créé avec **'' | ||
+ | |||
+ | Pour connaitre le chemin du dossier d' | ||
+ | < | ||
+ | go list -f ' | ||
+ | </ | ||
+ | |||
+ | Vérifier que le dossier d' | ||
+ | |||
+ | <code powershell> | ||
+ | echo $env:PATH | ||
+ | </ | ||
+ | |||
+ | La variable PATH doit contenir le dossier d' | ||
+ | |||
+ | Si vous souhaitez installer le binaire dans un répertoire différent de votre PATH, vous pouvez modifier/ | ||
+ | |||
+ | < | ||
+ | go env -w GOBIN=C: | ||
+ | </ | ||
+ | |||
+ | Une fois le chemin d' | ||