Ce tutoriel propose de créer deux modules:
Dans ce tutoriel on va :
En Go, le module est l'unité de distribution du code. Il permet de distribuer une collection de paquetages. Par exemple on peut vouloir créer un module contenant des paquetages groupant des fonctions d'analyse financière. Toutes les applications ayant besoin de faire de l'analyse financière pourront bénéficier du code. Pour plus d'informations voir la documentation officielle développement et publication des modules.
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'exécution du code incluant la version de Go et l'ensemble des autres modules requis.
Lorsqu'on ajouter ou améliore les fonctionnalités d'un module, on publie une nouvelle version du module. Les développeurs qui appellent/utilisent les fonctions de votre module mettent à jour les paquetages et testent les nouvelles versions avant de passer en production.
Déclarer votre nouveau module en utilisant la commande go mod init
et en fournissant le chemin vers le module. Si vous souhaitez publier votre module se devra être un chemin depuis lequel le module sera téléchargeable par les outils Go : cela pourrait être votre dépôt de code.
# créer un repertoire pour le code mkdir greetings && cd greetings # initialise le module go mod init example.com/greetings
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.
cat go.mod module example.com/greetings 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 :
package greetings import "fmt" // Hello retourne un message de bienvenue à la personne désignée func Hello( name string) string { message := fmt.Sprintf("Salut, %v. Bienvenue!", name) return message }
Dans ce code on a:
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é “export des noms”.
L'opérateur :=
est un raccourci permettant de déclarer et d'initialiser une variable en une seule ligne. Go utilise l'instruction à droite de l'opérateur pour déterminer le type de la variable.
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
cd .. mkdir hello
Après cette opération on a donc l'arborescence suivante
. ├── grettings │ ├── go.mod │ └── greetings.go └── hello
On entre dans le répertoire hello et on initialise le suivi du module :
go mod init example.com/hello
Ouvrir l'éditeur et créer le fichier hello.go avec le contenu suivant :
package main import ( "fmt" "example.com/greetings" ) func main() { msg := greetings.Hello("Yoann") fmt.Println(msg) }
Dans ce code :
On doit maintenant éditer le module “example.com/hello” pour qu'il utilise le module local “example.com/greetings”.
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 “example.com/hello” pour qu'il puisse trouver le module “example.com/greetings” sur le système de fichier local. Depuis la ligne de commande :
go mod edit -replace example.com/greetings=../grettings
La commande spécifie que “example.com/greetings” devra être remplacé par “../grettings” lors du calcul de dépendances. Après exécution de la commande, le fichier go.mod doit contenir une directive replace :
cat go.mod module example.com/hello go 1.20 replace example.com/greetings => ../greetings
Toujours depuis la ligne de commande dans le répertoire hello, exécuter la commande go mod tidy
pour synchroniser les dépendances du module “example.com/hello”, ajoutant ce qui est requis par le code mais pas encore suivi dans le module.
go mod tidy
Après exécution de la commande le ficheir go.mod doit être de la forme :
module example.com/hello go 1.20 replace example.com/greetings => ../grettings require example.com/greetings v0.0.0-00010101000000-000000000000
La commande a bien trouvé le code dans le répertoire local ../grettings et introduit la directive require
. Cette dépendance a été créée par l'import du paquetage greetings dans hello.go
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 versionning des modules.
go run .
Salut, Yoann. Bienvenue!
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 'greetings' et sa gestion par le module appelant 'main'.
Via l'éditeur, on modifie le fichier greetings/greetings.go
comme proposé ci-dessous:
package greetings import ( "errors" "fmt" ) // 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 "", errors.New("empty name") } // If a name was received, return a value that embeds the name // in a greeting message. message := fmt.Sprintf("Hi, %v. Welcome!", name) return message, nil }
A propos des modifications apportées:
On peut à présent modifier le code appelant dans le fichier hello/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)) msg, err := greetings.Hello("") //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(msg) }
Dans le code ci-dessus on a :
Depuis la ligne de commande, on peut exécuter notre code:
go run . greetings: empty name exit status 1
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
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))] }
Quelques remarques:
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é ;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.
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!
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
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))] }
Dans ce code nous avons:
make(map[key-type]value-type)
. La fonction HelloAll() retourne le map à l'appelant.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’identifiant spécial '_' pour être ignoré. Dans notre fichier hello.go nous pouvons à présent faire quelques changements :
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) }
Les changements sont les suivants :
Exécutons le code ainsi modifié :
go run . map[Alfred:Heureux de vous revoir Alfred! Emilie:Hello, Emilie. Bienvenue! John:Heureux de vous revoir John! Yoann:Salut Yoann, enchanté!]
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.
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 ;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) } }
Dans ce code on a :
Depuis la ligne de commande on peut lancer les tests:
go test
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:
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
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.
go build
compile les paquetages et les dépendances, produit un binaire mais n'installe pas le résultat ;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:
go list -f '{{.Target}}'
Vérifier que le dossier d'installation de Go est présent dans le PATH :
echo $env:PATH
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
go env -w GOBIN=C:\path\to\your\bin
Une fois le chemin d'installation identifier/redéfinit vous pouvez invoquer la commande go install
.