Outils pour utilisateurs

Outils du site


dev:go:tutoriels:ecrire_un_module_en_go

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
dev:go:tutoriels:ecrire_un_module_en_go [2023/08/03 16:47] yoanndev:go:tutoriels:ecrire_un_module_en_go [2023/08/06 10:13] (Version actuelle) yoann
Ligne 1: Ligne 1:
 {{tag>dev module tutoriel go}} {{tag>dev module tutoriel go}}
- 
-:TODO: 
  
 ====== Écrire un module en Go ====== ====== Écrire un module en Go ======
Ligne 215: Ligne 213:
  log.SetFlags((0))  log.SetFlags((0))
  
- msg, err := greetings.Hello("Yoann")+ msg, err := greetings.Hello("")
  
  //Si une erreur est retournée, elle est affichée dans la console  //Si une erreur est retournée, elle est affichée dans la console
Ligne 227: Ligne 225:
 } }
 </code> </code>
 +
 +Dans le code ci-dessus on a :
 +  * Configurer le paquetage "log" pour afficher le préfixe "greetings: " en entête des message de log et on a désactiver le timestamp et le nom du fichier source ;
 +  * Assigné les deux valeurs retournées par Hello à des variables ;
 +  * Modifié l'argument de la fonction Hello() en une chaîne vide pour provoquer l'erreur ;
 +  * Testé la valeur de l'erreur et terminer le programme dans le cas ou elle existe ;
 +  * Utilisé les fonctions du paquetage "log" de la bibliothèque standard de Go pour afficher les informations d'erreur et arrêter le programme avec log.Fatal().
 +
 +Depuis la ligne de commande, on peut exécuter notre code:
 +
 +<code bash>
 +go run .
 +greetings: empty name
 +exit status 1
 +</code>
 +
 +===== 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/suppressions d' éléments.
 +
 +Nous allons ajouter une slice contenant trois messages et le code permettant d'en retourner aléatoirement un parmi les trois.
 +
 +On ouvre l'éditeur et on modifie le fichier ''greetings/greetings.go''
 +
 +<code go greetings.go>
 +package greetings
 +
 +import (
 + "errors"
 + "fmt"
 + "math/rand"
 +)
 +
 +// 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 "", errors.New("empty name")
 + }
 +
 + message := fmt.Sprintf(randomFormat(), name)
 + return message, nil
 +}
 +
 +// randomFormat retourne aléatoirement un message préformatté parmi l'ensemble des messages disponibles.
 +func randomFormat() string {
 + formats := []string{
 + "Hello, %v. Bienvenue!",
 + "Heureux de vous revoir %v!",
 + "Salut %v, enchanté!",
 + }
 +
 + //retourne aléatoirement un format de message
 + return formats[rand.Intn(len(formats))]
 +}
 +</code>
 +
 +Quelques remarques:
 +  * La fonction randomFormat() commence par une minuscule la rendant accessible seulement par le code de son propre paquetage (en d'autres termes la fonction ne sera pas exportée).
 +  * La fonction randomFormat() déclare une slice ''formats'' contenant trois messages préformattés. Pour déclarer une slice, on ne spécifie pas le nombre d'éléments entre les crochets. Cela indique à Go que la taille du tableau contenu dans la slice peut changé ;
 +  * On utilise le paquetage "math/rand" pour générer un nombre aléatoirement afin de sélectionner un élément de la slice ;
 +  * 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:~/dev/go/hello$ go run .
 +Hello, Yoann. Bienvenue!
 +yoann@node-7c87:~/dev/go/hello$ go run .
 +Heureux de vous revoir Yoann!
 +yoann@node-7c87:~/dev/go/hello$ go run .
 +Heureux de vous revoir Yoann!
 +yoann@node-7c87:~/dev/go/hello$ go run .
 +Heureux de vous revoir Yoann!
 +yoann@node-7c87:~/dev/go/hello$ go run .
 +Hello, Yoann. Bienvenue!
 +</code>
 +
 +===== Retourner un message à plusieurs personnes =====
 +
 +Nous allons modifier une dernière fois nos modules afin d'obtenir des messages pour plusieurs personnes en une seule requête. Pour cela, nous allons devoir gérer plusieurs valeurs en entrée, associer à chacune une valeur et produire en sortie à valeurs multiples. En entrée on devra passer un ensemble de noms à la fonction qui retournera pour chacun des noms un message de bienvenue.
 +
 +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'écrire une nouvelle fonction avec un nom différent. Cette nouvelle fonction prendra plusieurs paramètres. Cela permet de préserver l'ancienne fonction et assure la rétrocompatibilité.
 +
 +Via l'éditeur, on modifie notre fichier greetings/greetings.go
 +<code go greetings.go>
 +package greetings
 +
 +import (
 + "errors"
 + "fmt"
 + "math/rand"
 +)
 +
 +// 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 "", errors.New("empty name")
 + }
 +
 + message := fmt.Sprintf(randomFormat(), name)
 + return message, nil
 +}
 +
 +// HelloAll retourne un map associant un message de bienvenue à chaque personne en entrée
 +func HelloAll(names []string) (map[string]string, error) {
 + //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 'names'
 + 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'ensemble des messages disponibles.
 +func randomFormat() string {
 + formats := []string{
 + "Hello, %v. Bienvenue!",
 + "Heureux de vous revoir %v!",
 + "Salut %v, enchanté!",
 + }
 +
 + //retourne aléatoirement un format de message
 + return formats[rand.Intn(len(formats))]
 +}
 +</code> 
 +
 +Dans ce code nous avons:
 +  * Ajouté une fonction HelloAll() ayant en paramètre une slice de plusieurs noms et retournant un map permettant d'associer un message (valeur) à chaque nom (clé) du map.
 +  * La nouvelle fonction HelloAll() appelle la fonction Hello(). Cet usage des fonction est aussi désigné factorisation et réduit les risques d'erreurs liés à la duplication de code.
 +  * Créé une variable "messages" de type map pour associer chaque message retourné par Hello() comme valeur à un clé (le nom passé en paramètre). En Go on initialise un map avec la syntaxe **''make(map[key-type]value-type)''**. La fonction HelloAll() retourne le map à l'appelant.
 +  * Exécuter une boucle sur chaque élément de la slice "names". Dans cette boucle **''for''** l'opérateur **''range''** retourne deux valeurs : l'index de l'élément courant dans la boucle et une copie de sa valeur. N'ayant pas besoin de l'index il a été affecté à l’[[https://go.dev/doc/effective_go.html#blank|identifiant spécial '_']] pour être ignoré.  
 +
 +Dans notre fichier hello.go nous pouvons à présent faire quelques changements :
 +
 +<code go hello.go>
 +package main
 +
 +import (
 + "fmt"
 + "log"
 +
 + "example.com/greetings"
 +)
 +
 +func main() {
 + //Définition de propriétés du Logger
 + log.Default().SetPrefix("greetings: ")
 + //Désactive l'afficahge du timestamp, du fichier source et du numero
 + //de ligne
 + log.SetFlags((0))
 +
 + //définition d'une slice contenant les noms
 + names := []string{"Yoann", "Alfred", "Emilie", "John"}
 +
 + //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'arrête
 + if err != nil {
 + log.Fatal(err)
 + }
 +
 + //Affiche le message dans la console
 + fmt.Println(messages)
 +}
 +</code>
 +
 +Les changements sont les suivants :
 +  * Création d'une variable "names" de type slice contenant 4 noms ;
 +  * Passage en argument de la variable "names" à la fonction HelloAll().
 +
 +Exécutons le code ainsi modifié :
 +<code bash>
 +go run .
 +map[Alfred:Heureux de vous revoir Alfred! Emilie:Hello, Emilie. Bienvenue! John:Heureux de vous revoir John! Yoann:Salut Yoann, enchanté!]
 +</code>
 +
 +===== Ajout d' un test unitaire =====
 +
 +Maintenant que notre code a implémenter les fonctionnalités souhaitées, ajoutons un test. Tester le code pendant le développement peut aider à révéler les bugs introduits lors des différentes modifications. Ici nous allons ajouter un test pour la fonction 'Hello()'.
 +
 +Go intègre le support des tests unitaires. Le paquetage "testing" de Go et la commande test s'appuient sur des conventions de nommage et permettent d'écrire et d'exécuter facilement et rapidement des tests. 
 +
 +  * Dans le répertoire greetings, créer un fichier ''greetings_test.go''. Terminer une fichier par le suffixe "_test.go" indique à la **commande test** de go que ce fichier contient des fonctions de test ;
 +  * Éditer le fichier greetings_test.go avec le contenu suivant :
 +
 +<code go greetings_test.go>
 +package greetings
 +
 +import (
 + "regexp"
 + "testing"
 +)
 +
 +// TestHelloName appelle Hello() avec un nom et vérifie
 +// la valeur retournée.
 +func TestHelloName(t *testing.T) {
 + aName := "Albert"
 + want := regexp.MustCompile(`\b` + aName + `\b`)
 + msg, err := Hello("Albert")
 +
 + if !want.MatchString(msg) || err != nil {
 + t.Fatalf(`Hello("Albert") = %q, %v, want match for %#q, nil`, msg, err, want)
 + }
 +}
 +
 +// TestHelloEmpty appelle Helo() avec une chaine vide doit
 +// retourner une erreur.
 +func TestHelloEmpty(t *testing.T) {
 + msg, err := Hello("")
 +
 + if msg != "" || err == nil {
 + t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
 + }
 +}
 +</code>
 +
 +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 'Test'. Les fonctions de test doivent en argument un pointeur vers le paquetage testing. Les méthodes de ce paramètre sont utilisées pour générer les rapports et les logs du test ;
 +  * 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 "t" et sa méthode "Fatalf()" pour afficher un message dans la console et interrompre l'exécution.
 +    * 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'erreur on utilise le paramètre "t" et la méthode "Fatalf()" pour afficher un message et interrompre l'exécution des tests.
 +
 +Depuis la ligne de commande on peut lancer les tests:
 +<code powershell>
 +go test
 +</code>
 +
 +La commande **go test** exécute les fonctions de test (ayant des noms préfixés par "Test") dans les fichiers  *_test.go. On peut ajouter l'argument -v (verbeux) pour avoir un retour plus détaillé de l'exécution des tests.
 +
 +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'anomalie:
 +
 +<code>
 + go test -v
 +=== RUN   TestHelloName
 +    greetings_test.go:16: Hello("Albert") = "Heureux de vous revoir %!v(MISSING)!", <nil>, want match for `\bAlbert\b`, nil
 +--- FAIL: TestHelloName (0.00s)
 +=== RUN   TestHelloEmpty
 +--- PASS: TestHelloEmpty (0.00s)
 +FAIL
 +exit status 1
 +FAIL    example.com/greetings   0.140s
 +</code>
 +
 +===== Compiler et installer l'application =====
 +
 +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 **''go build''** compile les paquetages et les dépendances, produit un binaire mais n'installe pas le résultat ;
 +  * La commande go **''install''** compile et installe le binaire.
 +
 +Lorsque le binaire est créé avec **''go build''** il peut être exécuté seulement depuis le répertoire courant ou si l'on précise son chemin complet (absolu). Installer l’exécutable consiste à placer le binaire dans un répertoire spécifique afin de pouvoir l'invoquer sans préciser son chemin complet.
 +
 +Pour connaitre le chemin du dossier d'installation utlisé par Go:
 +<code>
 +go list -f '{{.Target}}'
 +</code> 
 +
 +Vérifier que le dossier d'installation de Go est présent dans le PATH :
 +
 +<code powershell>
 +echo $env:PATH
 +</code>
 +
 +La variable PATH doit contenir le dossier d'installation de Go. Si ce n'est pas le cas modifier le PATH de l'utilisateur.
 +
 +Si vous souhaitez installer le binaire dans un répertoire différent de votre PATH, vous pouvez modifier/définir la valeur de la variable **''GOBIN''**
 +
 +<code>
 +go env -w GOBIN=C:\path\to\your\bin
 +</code>
 +
 +Une fois le chemin d'installation identifier/redéfinit vous pouvez invoquer la commande ''**go install**''.
 +
 +
 ===== Références ===== ===== Références =====
  
   * https://go.dev/doc/tutorial/create-module   * https://go.dev/doc/tutorial/create-module
dev/go/tutoriels/ecrire_un_module_en_go.1691081248.txt.gz · Dernière modification : 2023/08/03 16:47 de yoann