84
Ce document est la propriété exclusive de Jean-François Rouceau ([email protected]) - 01 avril 2013 à 19:35

Ce document est la propriété exclusive de Jean … system /linux/GNU_Linux... · 2016-06-18 · vers des langages moins efficaces en termes d’exécution, tels que Python ou JavaScript

Embed Size (px)

Citation preview

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

N°63 sommaire

GNU/Linux Magazine France Hors-sérieest édité par Les Éditions Diamond

B.P. 20142 – 67603 Sélestat CedexTél. : 03 67 10 00 20 – Fax : 03 67 10 00 21E-mail : [email protected] commercial : [email protected] : www.gnulinuxmag.com – www.ed-diamond.comDirecteur de publication : Arnaud MetzlerRédacteur en chef : Denis BodorSecrétaire de rédaction : Véronique SittlerRéalisation graphique : Kathrin Scali

Responsable publicité : Valérie Fréchard, Tél. : 03 67 10 00 27 [email protected] Service abonnement : Tél. : 03 67 10 00 20Impression : VPM Druck Rastatt / Allemagne

Distribution France : (uniquement pour les dépositaires de presse)MLP Réassort : Plate-forme de Saint-Barthélemy-d’Anjou. Tél. : 02 41 27 53 12Plate-forme de Saint-Quentin-Fallavier. Tél. : 04 74 82 63 04Service des ventes : Distri-médias : Tél. : 05 34 52 34 01IMPRIMÉ en Allemagne - PRINTED in Germany Dépôt légal : À parution, N° ISSN : 0183-0864Commission paritaire : K78 976Périodicité : BismestriellePrix de vente : 8,00 €

La rédaction n’est pas responsable des textes, illustrations et photos qui lui sont communiqués par leurs auteurs. La reproduction totale ou partielle des articles publiés dans GNU/Linux Magazine France Hors-série est interdite sans accord écrit de la société Les Éditions Diamond. Sauf accord particulier, les manuscrits, photos et dessins adressés à GNU/Linux Magazine France Hors-série, publiés ou non, ne sont ni ren-dus, ni renvoyés. Les indications de prix et d’adresses figurant dans les pages rédactionnelles sont données à titre d’information, sans aucun but publicitaire. Toutes les marques citées dans ce numéro sont déposées par leur propriétaire respectif. Tous les logos représentés dans le magazine sont la propriété de leur ayant droit respectif.

édito

iNtroductioN et NotioNs de base

4 GO, GO, GO !

7 UNE PETITE PARTIE DE GO ?

11 LA SYNTAXE DE BASE

Les VariabLes

17 LES DIFFÉRENTS TYPES DE VARIABLES

22 TOUT SAVOIR SUR LES CHAÎNES DE CARACTÈRES

27 GESTION DES POINTEURS

30 TABLEAUX, SLICES ET CARTES

Pour aLLer PLus LoiN

34 LES FONCTIONS

41 LES PAQUETAGES

47 LA PROGRAMMATION ORIENTÉE OBJET EN GO

55 LA GESTION DES ERREURS

58 TRAITEMENT DES FICHIERS

66 LES TESTS EN GO C'EST TELLEMENT SIMPLE QUE VOUS DEVRIEz LES TESTER !

72 MARRE DE LA ROUTINE ? PASSEz À LA CONCURRENCE AVEC LES GOROUTINES !

cas sPécifiques

79 INTERFACE GRAPHIQUE EN GTK+

80 ACCÈS À UNE BASE DE DONNÉES MYSQL

81 DÉVELOPPEMENT WEB

82 UTILISER LES ARGUMENTS DE LA LIGNE DE COMMANDES

aboNNeMeNts / coMMaNdes

15/45/46

Ce hors-série est entièrement consacré au Go. Oui, vous avez bien lu : le Go. Pourquoi parler de cela dans un beau numéro hors-série de GNU/Linux Magazine ? Denis Bodor serait-il devenu champion

du monde de Go et toute la rédaction le vénérerait-elle au point de lui dédier un hors-série ? Ça vous paraît absurde ? Pourtant, le Go, malgré des règles très simples, offre de grandes possibilités de développements stratégiques. On peut rapidement apprendre le Go, mais une maîtrise complète demandera quand même un peu de patience et d’entraînement. Ce jeu se joue à l’aide de petites pierres noires et blanches sur un plateau appelé goban. Chaque joueur se voit attribuer une couleur et doit placer ses pierres sur une intersection vide du goban, de manière à occuper le plus d’espace possible en formant des territoires les plus importants possibles. Au cours du jeu, on peut retirer des pierres adverses du goban si on les emprisonne : il suffit de les encercler et de les priver de leur dernière liberté (espace adjacent libre). Les pierres adjacentes d’une même couleur forment une chaîne et augmentent leur nombre de libertés. Si une pierre isolée a normalement quatre libertés, deux pierres adjacentes en ont six, et ainsi de suite. Il est donc intéressant de créer des chaînes pour limiter le risque de voir ses pierres emprisonnées et retirées du jeu. À la fin du jeu, on compte le nombre de prisonniers et la taille des territoires constitués par les pierres des deux adversaires.

D’un point de vue informatique, le jeu de Go est un problème complexe d’intelligence artificielle. En effet, contrairement aux échecs où l’algo-rithme du min-max permet d’obtenir de bons résultats en calculant un arbre des n prochains coups possibles et de déterminer quel déplacement engendrera le gain du plus de points possible, dans le jeu de Go le nombre de coups est tellement élevé et il est si difficile d’estimer les territoires que cette méthode échoue. À l’heure actuelle, ce sont les algorithmes basés sur la méthodes de Monte-Carlo qui donnent les meilleurs résultats. Pour simplifier, en fonction d’une partie en cours sur le goban, on génère des parties aléatoires, on détermine ensuite quel coup a produit le meilleur score moyen à la fin des parties obtenues en débutant par ce coup et on le joue. Un petit peu d’intelligence (heuristique) est bien sûr ajouté par la suite pour aider l’ordinateur à faire son choix...

Ah ? On vient de me prévenir que je m’étais totalement fourvoyé... Il ne s’agit pas du jeu de Go, mais du langage Go ! Je ne vais quand même pas tout ré-écrire ! Si vous ne voulez pas vous aussi vous perdre dans les méandres sans fin des règles et tactiques du jeu de Go, n’effectuez vos recherches web sur le langage Go qu’à l’aide du mot-clé « golang » et non « go », vous gagnerez beaucoup de temps...

Tristan Colombo

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com4

iNtroductioN & NotioNs de base Go, Go, Go !

Go, Go, Go ! par Tristan Colombo

La première version stable du langage Go a été publiée en mars de cette année. Il s’agit donc du langage le plus récent disponible pour une utilisation en production et comme avec tout nouveau langage, de nombreuses questions se posent. À quoi sert-il ? Pourquoi encore un langage différent ? Le projet est-il pérenne ? Comment l’utiliser ? Ce premier article tente de répondre à ces questions.

1 HistoriqueLe langage Go a tout d’abord été dé-

veloppé en interne chez Google à partir de 2007. L’équipe de développement était composée de Robert Griesemer, Rob Pike et Kenneth Thompson. Sans sous-estimer le talent de ses partenaires, le dernier nom devrait vous évoquer quelque chose... Mais si, cherchez au fond de votre mémoire, vous avez for-cément entendu parler à un moment ou à un autre de Kenneth (ou Ken) Thompson ! Il a travaillé avec Dennis Ritchie sur l’écriture du système Unix, il a créé le langage B qui a servi de base à l’écriture du langage C, c’est lui qui a inventé l’éditeur ed et, avec Rob Pike précédemment cité, le codage de caractères UTF-8, etc. Le nombre et la qualité de ses contributions en font une véritable légende, au même titre que Dennis Ritchie. Même si, dans notre société, ce sont les commerciaux ca-pables de vendre une fortune n’importe quel matériel estampillé d’un petit fruit grignoté par un ver qui apparaissent comme de grands informaticiens, il ne faut pas oublier que c’est grâce à des hommes de la trempe de Kenneth Thompson ou Dennis Ritchie que l’in-formatique (en tant que science) a pu progresser.

Le 10 novembre 2009, le projet fut rendu public sous licence libre BSD-style. À partir de ce moment, de nombreux contributeurs à travers le monde ont

participé à l’amélioration du langage. La licence choisie est une véritable licence libre, mais les puristes préfèrent éviter l’appellation « BSD-style » (voir à ce pro-pos une discussion sur le problème de la licence BSD [1]). Vous pourrez donc utiliser le langage Go sans mauvaise surprise : une licence libre, une com-munauté croissante d’utilisateurs, des concepteurs prestigieux et une grosse société poussant le projet... Les risques de voir le projet mourir sont faibles !

La mascotte du langage a été dessinée par Renée French [2] et représente un gopher, petit animal sympathique se rapprochant du chien de prairie (en plus petit). Avantage indéniable de cet animal : son nom contenant « go ».

À l’heure où ces lignes sont écrites, la dernière version stable du langage est la version 1.0.2 publiée le 13 juin 2012.

La mascotte du langage Go : le gopher

2 Domaines d’application

Avant d’aborder les domaines d’appli-cation proprement dits, essayons d’abord de répondre à la question « pourquoi avoir créé un nième langage ? ». Les concepteurs sont partis du constat que, de nos jours, pour choisir un langage de développement il fallait choisir entre une compilation efficace, une exécution efficace, ou un développement simple. Ces trois propriétés fondamentales n’étant présentes dans aucun langage, les développeurs préfèrent se tourner vers des langages moins efficaces en termes d’exécution, tels que Python ou JavaScript plutôt que le C++. J’avoue d’ailleurs faire partie de cette catégorie et ne remets nullement en question tout le bien que je pense de Python !

Le langage Go est donc une tentative d’alchimie entre, d’une part la simplicité de développement à l’aide d’un langage interprété et à typage dynamique et d’autre part, l’efficacité et la sécurité d’un langage compilé et à typage statique. Go essaye donc de tirer le meilleur des deux mondes.

Le langage Go est généraliste. Il peut donc être utilisé pour développer n’importe quel type d’application. Tou-tefois, d’un point de vue conceptuel, Go a été conçu pour le développement sur des machines possédant une

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 5

iNtroductioN & NotioNs de base Go, Go, Go !

architecture multicœur. De plus, le projet Go a été initié pour pouvoir écrire simplement le code des serveurs et des outils que Google utilise en interne. Donc, même si le langage Go permet de tout faire, originellement son domaine d’application touche à tout ce qui est système et serveur sur des architectures multicœurs.

3 InstallationLe compilateur standard du Go

s’appelle gc . Un autre compilateur est disponible, le gccgo qui est une sur-couche au compilateur gcc (GNU Compiler Collection). Suivant vos habi-tudes de développement, vous pourrez installer l’un ou l’autre.

3.1 Installation de gc3.1.1 Depuis les dépôts

Sur une distribution basée sur Debian, le paquetage permettant d’installer le langage est disponible. Il suffit de taper :

sudo aptitude install golang

La version installée sera plus ancienne qu’avec les sources, puisque vous dis-poserez ici de go1 (tapez la commande go version pour vérifier).

3.1.2 Depuis les sourcesAssurez-vous tout d’abord de posséder

les paquetages permettant de compiler du C. Sur une distribution basée sur Debian, vous devrez exécuter :

sudo aptitude install gcc libc6-dev

Vous aurez également besoin du gestionnaire de versions concurrentes Mercurial :

sudo aptitude install mercurial

Avant de récupérer le code du projet, nous allons paramétrer notre environne-ment à l’aide de variables qui seront lues par Go. Pour cela, ouvrez votre fichier ~/.bashrc et ajoutez en fin de fichier :

# Variables d’environnement pour Goexport GOROOT=/opt/goexport GOOS=linuxexport GOARCH=amd64export GOBIN=$GOROOT/binexport PATH=$PATH:$GOBIN

Pour activer ces modifications, n’oubliez pas d’exécuter la commande :

source ~/.bashrc

Voici la signification des différentes variables utilisées :

- GOROOT : répertoire contenant les sources du projet Go ;

- GOOS : système d’exploitation utilisé ;

- GOARCH : architecture utilisée. Si vous êtes sur une architecture 32 bits, vous choisirez 386. Pour une architecture 64 bits, vous choisirez comme moi amd64 ;

- GOBIN : répertoire contenant les fichiers binaires du projet Go. Ce réper-toire correspond la plupart du temps au sous-répertoire bin du répertoire contenant les sources, d’où l’utilisation de la variable GOROOT définie précédemment ;

- PATH : variable bien connue indiquant les répertoires accessibles direc-tement pour l’exécution de code. On ajoute ici le répertoire de la variable GOBIN.

Vous pouvez ensuite lancer la commande permettant de récupérer le code source du projet :

sudo hg clone -u release https://code.google.com/p/go $GOROOT

Si vous avez suivi la même configuration que moi, le code sera placé dans le répertoire /opt/go. Déplacez-vous alors dans le répertoire des sources et lancez la compilation :

cd /opt/go/srcsudo ./all.bash

À la fin de cette étape, vous obtiendrez un message du type :

ALL TESTS PASSED --- Installed Go for linux/amd64 in /opt/go Installed commands in /opt/go/bin *** You need to add /opt/go/bin to your PATH.

Ne tenez pas compte de l’avertissement, puisque nous avons déjà ajouté le répertoire /opt/go/bin ($GOROOT/bin ou $GOBIN) à notre variable d’environ-nement PATH.

Pour finir, donnez les droits d’accès et d’exécution aux fichiers du répertoire go pour tous les utilisateurs :

sudo chmod -R ugo+rx /opt/go

Vous venez d’installer Go en version 1.0.2 !

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com6

Par la suite, pour maintenir votre installation à jour, vous devrez exécuter :

cd $GOROOT/srchg pullhg update release./all.bash

3.1.3 Test de l’installationNous testerons plus tard notre premier programme.

Pour l’instant, pour vérifier que votre installation est correcte, vous pouvez essayer de visualiser localement la documentation du projet. En effet, le site officiel de Go, http://www.golang.org, héberge la documentation du langage... dont le serveur est écrit en Go. Pour lancer lo-calement ce serveur, tapez la commande suivante :

godoc -http=:8000

Vous aurez alors accès à la documentation en connec-tant un navigateur sur l’adresse 127.0.0.1:8000. Si vous le souhaitez, vous pourrez bien sûr modifier le port de connexion.

3.2 Installation de gccgo3.2.1 Depuis les dépôts

Là encore, sur une distribution basée sur Debian, le paquetage est disponible :

sudo aptitude install gccgo

3.2.2 Depuis les sourcesGcc utilise encore le gestionnaire de versions concurrentes

Subversion... Il y a fort à parier que vous ne l’ayez pas sur votre système et il faut donc commencer par l’installer :

sudo aptitude install subversion

Vous pourrez ensuite récupérer les sources :

svn checkout svn://gcc.gnu.org/svn/gcc/trunk mon_gcc

Créez alors un répertoire objdir, puis définissez vos options à l’aide de la commande configure :

cd mon_gccmkdir objdircd objdir../configure --enable-languages=c,c++,go

Ici, j’ai activé le support des langages C, C++ et Go, mais vous pouvez bien sûr choisir les langages que vous souhaitez. La liste de toutes les options disponibles est affichée par la commande :

../configure --help

Cette étape achevée, il ne vous reste plus qu’à retourner dans le répertoire principal des sources et à lancer l’installation :

cd ..sudo ./install-sh

4 Pouvoir utiliser des programmes Go comme des scripts

La compilation des fichiers Go se fait très, très rapidement (c’était un objectif du langage). Il est alors possible d’utiliser Go comme s’il s’agissait d’un langage de script : au premier lancement, la compilation sera effectuée et le programme exécuté. Tant que le programme ne sera pas modifié, il n’y aura pas de nouvelle compilation. C’est comme si vous aviez écrit un Makefile pour un programme C et un exécutable pour appeler ce Makefile et votre programme compilé.

Le programme permettant de réaliser cela n’est pas distribué avec Go et il faut donc l’installer par la commande :

sudo $GOBIN/go get github.com/kless/goplay

Nous avons utilisé ici la commande go get qui permet d’installer un paquetage ainsi que toutes ses dépendances. C’est l’équivalent du pip install en Python.

Pour utiliser ce programme, il vous suffira alors d’ajouter en tête de vos programmes le classique shebang qui aura la forme :

#!/usr/bin/env goplay

Et bien sûr, vous devrez rendre vos fichiers exécutables... Nous testerons cette pratique dans le prochain article.

ConclusionL’environnement de travail est prêt : nous allons pouvoir

passer à l’expérimentation sur de petits exemples pour ap-préhender la syntaxe de base et la compilation de fichiers Go.

Pour plus de clarté, le même compilateur sera utilisé dans tous les articles de ce hors-série : gc, avec Go en version 1.0.2.

Références[1] Problème de la licence BSD :

http://www.gnu.org/philosophy/bsd.fr.html

[2] Blog de la dessinatrice Renée French : http://reneefrench.blogspot.fr/

iNtroductioN & NotioNs de base Go, Go, Go !

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 7

iNtroductioN & NotioNs de base

Une PeTITe ParTIe De Go ?par Tristan Colombo

Après avoir installé le compilateur, voici venu le temps des premiers tests et du classique « Hello world ». Pour ces premiers tests, nous allons focaliser notre attention sur la syntaxe de base, la manière d’écrire du code et la compilation. Notre point d’entrée pour ces tests sera un petit programme « Hello world » que nous ferons évoluer et qui nous permettra d’introduire, de manière basique, quelques notions qui seront approfondies plus tard, dans d’autres articles. Le compilateur est installé et fonctionnel : en avant pour le grand saut.

1 Hello worldLes fichiers Go portent l’extension .go. Nous allons

commencer par créer un fichier hello.go contenant le code suivant :

01: package main 02: 03: import "fmt" 04: 05: func main() 06: { 07: fmt.Println("Hello world!") // Affichage du message08: }

Première remarque qui vous est peut être apparue alors que vous tapiez ces quelques lignes : le support de la coloration syntaxique du langage Go dans votre éditeur. Peu d’éditeurs disposent en effet automatiquement de la coloration syntaxique pour ce langage. Gedit fait partie de ces rares éditeurs, mais si vous utilisez Vim ou Eclipse par exemple, il va falloir ajouter une extension. Pour les autres éditeurs, je vous conseille de consulter la page http://go-lang.cat-v.org/text-editors.

1.1 La coloration syntaxique sous Vim

La distribution standard de Go contient un répertoire contenant tous les outils nécessaires pour configurer Vim. Ce répertoire se nomme $GOROOT/misc/vim.

Il contient tout d’abord les fichiers de coloration syntaxique sur lesquels nous allons créer des liens symboliques

(comme ça, lors de la mise à jour de Go, en cas de modi-fication de ces fichiers, votre configuration de Vim sera également mise à jour sans autre manipulation) :

mkdir ~/.vim/ftdetectln -s $GOROOT/misc/vim/ftdetect/gofiletype.vim ~/.vim/ftdetectmkdir ~/.vim/syntaxln -s $GOROOT/misc/vim/syntax/go* ~/.vim/syntax/

Dans votre fichier ~/.vimrc, vous devez bien sûr avoir ajouté les lignes permettant de détecter les types de fichiers et avoir activé la coloration syntaxique :

filetype plugin indent onsyntax on

Vous pourrez ajouter par la suite le support de la do-cumentation avec godoc :

ln -s $GOROOT/misc/vim/plugin/godoc.vim ~/.vim/pluginln -s $GOROOT/misc/vim/autoload/go ~/.vim/autoload

Le fichier godoc.vim définit un raccourci clavier pa-ramétrable pour appeler la fonction d’affichage de l’aide :

nnoremap <silent> <Plug>(godoc-keyword) :<C-u>call <SID>Godoc(‘’)<CR>

Pour pouvoir l’utiliser, dans Vim, il va vous falloir définir un raccourci clavier utilisant ce raccourci en écrivant dans votre fichier ~/.vimrc quelque chose du type :

nmap <C-g> <Plug>(godoc-keyword)

Ici, c’est la combinaison des touches [Ctrl]+[g] qui per-mettra d’afficher l’aide correspondant au terme se trouvant sous le curseur.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com8

iNtroductioN & NotioNs de base uNe Petite Partie de Go ?

1.2 La coloration syntaxique sous eclipse

Ajoutez l’adresse http://goclipse.googlecode.com/svn/trunk/goclipse-update-site/ dans le gestionnaire de mises à jour et installez l’extension goclipse. Cette extension, outre la coloration syntaxique, vous donnera accès à l’auto-complétion et à la documentation des différentes fonctions. Vous aurez bien sûr accès aux commandes de compila-tion et d’exécution depuis Eclipse, ainsi qu’à l’affichage des messages d’erreurs.

1.3 Étude et compilation du code

Maintenant que nos outils de travail sont correctement configurés, revenons sur le code.

On remarque tout de suite qu’à l’ins-tar de Python, il n’y a pas de caractère indiquant la fin de ligne. Le code se décompose en trois parties :

- package main : définition d’un paquetage. En Go, on raisonne en termes de paquetages et non de fichiers. Ainsi, un même paquetage pourra être composé de plusieurs fichiers, mais un fichier appartient forcément à un paquetage. Le paquetage main est le paquetage d’entrée dans un projet.

- import : chargement d’un ou plu-sieurs paquetage(s). Cette commande fonctionne de la même manière que le import de Python pour les modules : vous devrez préfixer le nom des fonctions utilisées par le nom du paquetage.

- func main() : déclaration d’une fonction main() qui sera exécutée automatiquement à l’exécution du programme. Cette fonction, qui n’accepte aucun argument et ne renvoie rien, contient un appel à la fonction Println (avec une majus-cule) du paquetage fmt. C’est elle qui affiche la chaîne de caractères à l’écran. Notez au passage que les chaînes de caractères ne peuvent être délimitées que par des guillemets.

La ligne 7 contient un commentaire. Les commentaires sont notés de la même manière qu’en C :

- // pour commenter tout ce qui suit jusqu’à la fin de la ligne ;- /* ... */ pour commenter un bloc de code.

Essayons maintenant de lancer notre programme à l’aide de la commande suivante :

go run hello.go

Cette commande va compiler et exécuter le code directement, vous ne conser-verez pas le fichier exécutable.

Au lancement de la commande, première surprise : ça ne marche pas !

# command-line-arguments ./hello.go:6: syntax error: unexpected semicolon or newline before {

Le compilateur nous indique une erreur de syntaxe due à la position de notre accolade. En fait, Go n’admet qu’une seule manière de présenter le code : le style K&R (pour Kernigham et Ritchie), où les accolades sont ouvertes en fin de ligne avant le bloc qu’elles définissent. Notre code devient alors :

01: package main 02: 03: import "fmt" 04: 05: func main() {06: fmt.Println("Hello world!") // Affichage du message07: }

Cette fois-ci, après appel à go run hello.go, nous obtenons bien l’affichage du message « Hello world ! ».

2 Présentation du codeAvec Go, vous pouvez vous permettre de ne pas indenter et présenter propre-

ment votre code (bien que cela soit réalisable assez simplement et rapidement) : un utilitaire fera le travail pour vous ! Cet utilitaire se nomme gofmt et il forma-tera automatiquement votre code. Si vous le testez sur le code hello.go, il vous affichera ce même code, mais avec une indentation de la taille de huit caractères espace (la valeur par défaut). L’appel se fait simplement par :

gofmt hello.go

Cet utilitaire propose de nombreuses options. On peut noter :

- -tabwidth=n qui détermine le nombre de caractères espace à utiliser pour indenter le code. La valeur par défaut est 8 ;

- -tabs=true (ou false) qui indique si les indentations sont réalisées à l’aide d’espaces ou de tabulations ;

- -comments=true (ou false) qui affiche ou non les commentaires du code ;

- -w=false (ou true) qui détermine si les modifications de la commande gofmt doivent être enregistrées dans le fichier source ou non. Par défaut, les modifications sont affichées à l’écran et cette option a pour valeur false.

Voici un exemple d’utilisation de cet utilitaire sur notre code, en deman-dant de créer des indentations composées de quatre caractères espace, de

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 9

iNtroductioN & NotioNs de base uNe Petite Partie de Go ?

supprimer les commentaires et de sauvegarder les modifications dans notre fichier source :

gofmt -tabwidth=4 -tabs=false -comments=false -w=true hello.go

Le fichier hello.go contient main-tenant le code suivant :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: fmt.Println("Hello world!") 07: }

Sur notre exemple contenant peu de code, les effets du reformatage sont très limités mais gofmt est capable d’aligner des déclarations de variables, des com-mentaires, d’ajouter des espaces avant et après les opérateurs, etc.

En Go, il n’y a pas de recommandation sur la longueur maximale d’une ligne. Lorsque vous trouvez votre ligne trop longue, il vous suffit d’aller à la ligne et d’ajouter une indentation par rapport à l’indentation précédente (pour une meilleure lisibilité). Par exemple, si nous voulons scinder la ligne suivante :

06: fmt.Println("Hello world!")

Nous écrirons :

06: fmt.Println( 07: "Hello world!")

3 Compilation approfondie

Nous avons utilisé la commande go run pour compiler et exécuter notre code, mais il existe trois autres possibilités.

3.1 Compilation sans exécution

La commande go build va permettre de simplement compiler le code. Si vous exécutez cette commande sur le fichier hello.go, vous obtiendrez un fichier exécutable hello.

3.2 Compilation et installation

La commande go install va compiler le code et placer le fichier exécutable dans votre répertoire $GOBIN (si vous avez suivi la même architecture que moi ce sera /opt/go/bin). Comme $GOBIN doit se trouver dans votre variable d’environnement PATH , vous aurez accès directement à votre exécutable depuis n’importe quel répertoire. La commande complète à exécuter pour installer notre exemple est :

go install hello.go

Attention : votre répertoire $GOBIN doit être accessible en lecture ! Donc, il faut soit modifier ses droits d’accès (sudo chmod ugo+x $GOBIN), soit lancer la commande d’installation en tant que root en ayant pensé à définir les variables d’environnement relatives au langage Go dans le fichier .bashrc de l’administrateur.

3.3 Compilation enfouieNous avons vu dans l’article précédent

que grâce au programme goplay, nous pouvons utiliser nos codes Go comme des scripts. Pour tester ce fonctionne-ment sur le code de hello.go, il suffit d’ajouter la ligne du shebang :

01: #!/usr/bin/env goplay02: 03: package main 04: 05: import "fmt" 06: 07: func main() { 08: fmt.Println("Hello world!") 09: }

Rendez ensuite le fichier exécutable :

chmod ugo+x hello.go

Vous pouvez maintenant taper direc-tement hello.go pour exécuter votre code (il est possible que vous obteniez un message d’erreur à cause d’un réper-toire $GOROOT/pkg non accessible en écriture... Le problème se règle comme précédemment à l’aide de chmod).

4 organisation du code

Jusqu’à présent, nous avons travaillé directement à la racine du langage Go. Vous avez vu que pour pouvoir installer ou exécuter des programmes en tant que scripts, il a fallu modifier les droits d’accès de certains répertoires. C’est la façon la plus simple de compiler et d’installer les programmes, mais on peut faire beaucoup plus proprement...

Go permet de définir une variable d’environnement GOPATH qui indique une liste de répertoires identifiés en tant qu’espaces de travail. Chacun de ces répertoires représente donc un projet Go et l’organisation classique d’un projet va comporter trois répertoires :

- un répertoire src qui va contenir le code source (un sous-répertoire par paquetage) ;

- un répertoire pkg qui permettra de stocker le code compilé pour les paquetages employés ;

- un répertoire bin qui va contenir les exécutables.

Pour l’instant, notre exemple est très simple et ne contient pas de paquetages, mais nous pouvons tout de même tester cette organisation. Créons un répertoire go_samples ayant la structure suivante :

go_samples/ ├── bin ├── pkg └── src └── hello └── hello.go

Ici nous avons défini un environ-nement de travail go_samples qui contient un projet hello se situant dans le répertoire src. Ce projet ne contient qu’un programme principal, qui est donc défini à la racine src/hello. Si nous avions eu des paquetages, nous aurions créé des sous-répertoires dans hello.

Pour que cette arborescence soit prise en compte par la commande go, nous devons définir et modifier certaines variables d’environnement.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com10

iNtroductioN & NotioNs de base uNe Petite Partie de Go ?

Tout d’abord, il faut définir GOPATH et modifier PATH (toujours dans le fichier ~/.bashrc) :

export GOPATH=~/.../go_samplesexport PATH=$PATH:$GOPATH/bin

Vous remarquerez que nous n’utilisons plus la variable GOBIN pour définir PATH : cette variable doit être supprimée si vous ne voulez pas que Go utilise toujours le répertoire $GOBIN pour ces installations. Pour supprimer la variable dans votre session courante, vous pouvez lancer un unset GOBIN.

Une fois ces modifications effectuées, placez-vous dans le répertoire go_samples et tapez la commande d’installation :

go install hello

Vous obtiendrez alors le fichier bin/hello qui pourra être lancé depuis n’importe quel répertoire. Si nous avions défini des paquetages, nous aurions également obtenu des fichiers objets dans le répertoire pkg. Nous reviendrons plus tard sur cette notion.

5 Modifications du code « Hello world »

Maintenant que le code est correctement organisé et que nous savons compiler et installer, essayons-nous à quelques petites modifications du code d’exemple.

5.1 Prise en charge des caractères Unicode

La documentation du langage propose un exemple « hello world » légèrement différent du nôtre. En effet, pour mon-trer le support des caractères Unicode, le mot « world » est écrit en chinois (ici, il ne s’agit que de caractères chinois sans signification) :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: fmt.Println("Hello 矍花!") 07: }

L’exécution de ce code produira bien l’affichage attendu.

5.2. Paramètres en ligne de commandes

Nous allons maintenant afficher un message paramétré par une valeur saisie lors de l’appel de notre programme en ligne de commandes :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: ) 07: 08: func main() { 09: fmt.Println("Hello", os.Args[1]) 10: }

Nous avons utilisé un nouveau paquetage nommé os (ligne 5). Vous remarquerez que pour importer un seul paquetage les parenthèses sont inutiles, alors qu’elles sont employées ici en lignes 3 et 6. Les arguments de la ligne de commandes sont récupérés dans un objet os.Args où os.Args[0] est le nom de la commande, os.Args[1] correspond au premier argument, etc. La numérotation suit la numérotation du shell avec $0, $1, etc. Nous affichons ici seulement le premier argument grâce à la fonction fmt.Println(), qui réalise une concaténation des paramètres qui lui sont transmis, à savoir « Hello » et os.Args[1]. Nous approfondirons plus tard la structure de tableau utilisée par os.Args.

En l’état de nos connaissances, nous ne pouvons pas en-core gérer les erreurs, donc si le programme est appelé sans argument, vous obtiendrez un message du type :

panic: runtime error: index out of range goroutine 1 [running]: main.main() /.../go_samples/src/hello/hello.go:9 +0x8d

goroutine 2 [syscall]: created by runtime.main /opt/go/src/pkg/runtime/proc.c:221

le shell a retourné 2

Pour que le programme fonctionne, il faudra donc l’appeler avec un argument, comme dans : hello GLMF.

ConclusionCe premier exemple nous a permis de bien manipuler et

d’apprivoiser la commande go et d’organiser notre code sous forme d’environnements de travail. Nous avons pu voir la simplicité avec laquelle la compilation pouvait être effectuée, réalisant le même travail qu’une compilation C classique avec un fichier Makefile mais où, justement, nous n’avons pas besoin d’écrire de Makefile. C’est le « easy to build » (facile à construire) du slogan de Go : « Go is an open source pro-gramming environment that makes it easy to build simple, reliable and efficient software » (Go est un environnement de programmation open source qui rend facile la construction d’un programme simple, fiable et efficace).

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 11

iNtroductioN & NotioNs de base

La synTaxe De base par Tristan Colombo

Les structures de contrôle sont essentielles pour pouvoir programmer : sans la possibilité d’effectuer des branchements logiques à l’aide de structures de test ou la possibilité de répéter des instructions, impossible de développer quoi que ce soit...

L ’objectif du langage Go étant de simplifier le dévelop-pement, ne vous étonnez pas de ne pas retrouver des dizaines de mots-clés différents pour effectuer des

tests ou des boucles. Par contre, la variété apparaîtra grâce à des syntaxes différentes. On peut trembler en se disant que le code va rapidement devenir illisible et in-maintenable... Mais il n’en est rien ! Vous allez voir que tout a été bien pensé.

1 structures de testLe langage Go propose deux structures permettant d’effec-

tuer des tests classiques (une troisième structure permettant de travailler sur les canaux (channels) sera vue plus tard).

1.1 Le ifLe test avec l’instruction if se fait de manière très simple

avec un bloc du type :

if condition { // Traitement} else { // Traitement}

La condition n’est pas encadrée de parenthèses comme c’est le cas dans une écriture Python. Voici un exemple d’application dérivé de notre précédent « hello world » :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: ) 07: 08: func main() { 09: if len(os.Args) == 2 {10: fmt.Println("Hello", os.Args[1]) 11: } else {12: fmt.Println("Vous devez spécifier un argument (et un seul)!") 13: }14: }

Le test porte sur le nombre d’arguments passés à notre programme lors de l’appel depuis la ligne de commandes.

Ce nombre s’obtient par l’instruction len(os.Args) en ligne 9. On demande en fait le nombre d’éléments du tableau os.Args. S’il contient exactement deux éléments, nous affichons notre message « Hello » paramétré par la chaîne saisie par l’utilisateur (ligne 10), sinon nous affi-chons un message d’erreur (ligne 13).

Les opérateurs de test sont les opérateurs classiques du C : == pour l’égalité, != pour la différence, <, >, <=, >=, etc. L’opérateur d’identité n’existe pas en Go (si vous ne savez pas ce que c’est, inutile de rentrer dans le détail puisque vous ne pourrez pas l’utiliser...).

Il est possible de chaîner des tests en utilisant des if/else imbriqués :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: ) 07: 08: func main() { 09: if len(os.Args) == 1 {10: fmt.Println("Vous devez spécifier un argument!") 11: } else if len(os.Args) == 2 {12: fmt.Println("Hello", os.Args[1]) 13: } else {14: fmt.Println("Vous ne devez spécifier qu’un argument!") 15: }16: }

Si le programme est appelé sans argument (test de la ligne 9), on affiche un message d’erreur pour manque d’informations, sinon, s’il est appelé avec un argument (ligne 11), on affiche le « hello » personnalisé et, sinon, dans tous les autres cas (ligne 13), on affiche un message d’erreur pour informations superflues.

Ce que nous avons vu est la forme de base du traitement if en Go. En fait, cette instruction admet une instruction qui sera exécutée en initialisation du test (donc avant d’effectuer le test). La structure du if devient alors :

if pré-traitement; condition { // Traitement}

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com12

iNtroductioN & NotioNs de base La syNtaxe de base

L’instruction de pré-traitement et la condition sont séparées par un point-virgule. Modifions notre exemple précédent de manière à ce qu’il affiche un message avant d’effectuer ces tests :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: ) 07: 08: func main() { 09: if fmt.Println("Test du nombre d’arguments"); len(os.Args) == 2 {10: fmt.Println("Hello", os.Args[1]) 11: } else { 12: fmt.Println("Vous devez spécifier un argument (et un seul)!") 13: } 14: }

La ligne 9 indique qu’il faut afficher le message « Test du nombre d’arguments » avant d’effectuer le test.

L’utilité de ce mécanisme dans l’exemple précédent est assez limité... Je vais donc devoir utiliser une variable, même si celles-ci ne seront vues en détail que dans le prochain article. La définition d’une variable à typage dynamique se fait à l’aide de l’opérateur :=. Nous n’aurons besoin que de cette notion pour notre exemple. Si une variable est définie dans le traitement optionnel d’un if, cette dernière n’existera que dans le bloc du test :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: if a := 2; a < 0 {07: fmt.Println("a est négatif:", a) 08: } else { 09: fmt.Println("a est positif ou nul:", a) 10: } 11: // fmt.Println("Valeur de a:", a) 12: }

En ligne 6, nous définissons une variable a qui n’existe que dans le bloc du if, soit dans les lignes 6 à 10. La ligne 11 est mise en commentaire, car si elle est activée, elle provoquera une erreur, la variable a n’existant pas.

Pour réaliser le même programme sans utiliser cette structure, il faudra utiliser une écriture beaucoup plus lourde avec création d’un pseudo-bloc pour limiter la visibilité de la variable a :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: { 07: a := 2

08: if a < 0 { 09: fmt.Println("a est négatif:", a) 10: } else { 11: fmt.Println("a est positif ou nul:", a) 12: } 13: } 14: // fmt.Println("Valeur de a:", a) 15: }

Le pseudo-bloc est ouvert en ligne 6, puis fermé en ligne 13. Je parle de pseudo-bloc, car il n’a pas d’autre utilité que de limiter la visibilité d’un objet ; il n’est pas consécutif à une instruction de contrôle. La variable a est définie en ligne 7, puis le test est effectué en ligne 8. Là encore, la ligne 14 essayant d’afficher la valeur de a en dehors de sa zone de visibilité est commentée pour pouvoir exécuter le code.

Le code précédent, utilisant la clause optionnelle, est ainsi plus synthétique tout en gardant une bonne lisibilité. Un mécanisme à ne pas oublier...

1.2 Le switchLe switch du Go est plus généraliste que le switch du C.

Cette instruction pourra certes être utilisée pour détecter si une variable a telle ou telle valeur (sachant que contrairement au C, il n’y aura pas de limitation de type), mais également d’effectuer simplement des suites de tests permettant d’écrire un équivalent de if/else imbriqués.

Voici un exemple d’utilisation similaire à celle qui en est faite en C :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: switch a {08: case 1: fmt.Println("Bloc pour le cas 1")09: fmt.Println("a vaut 1") 10: case 2: fmt.Println("a vaut 2")11: default: fmt.Println("Le reste...")12: }13: }

Le switch commence en ligne 7 et finit en ligne 12. Vous remarquerez l’absence d’instructions break pour terminer le traitement d’un cas : chaque cas est isolé naturellement et si vous souhaitez exécuter plusieurs instructions, il suffit de les mettre à la suite comme dans les lignes 8 et 9. Le cas default de la ligne 11 est exécuté quand la valeur de la variable a est différente de tous les cas énoncés précédemment.

Comme avec l’instruction if où nous avions pu définir une variable visible uniquement dans le bloc de test, nous pouvons définir une variable en phase d’initialisation qui ne pourra être utilisée que dans le switch :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 13

iNtroductioN & NotioNs de base La syNtaxe de base

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: switch a := 1; a {07: case 1: fmt.Println("Bloc pour le cas 1") 08: fmt.Println("a vaut 1. Vérification:", a) 09: case 2: fmt.Println("a vaut 2. Vérification:", a) 10: default: fmt.Println("Le reste...") 11: }12: // fmt.Println("Valeur de a:", a) 13: }

La variable a n’est accessible qu’entre les lignes 6 et 11.

Enfin, utilisé sans variable de référence, le switch vérifie des conditions :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: switch {08: case a > 0: fmt.Println("a positif") 09: case a == 5: fmt.Println("a vaut 5. Vérification:", a) 10: default: fmt.Println("Le reste...") 11: }12: }

Ici, ce sont seulement les tests qui indiqueront si les ins-tructions doivent être exécutées. En ligne 8, si la valeur de la variable a est supérieure à 0, alors le message « a positif » sera affiché. Attention toutefois, une fois qu’un bloc d’instruc-tions est exécuté, nous sortons du switch. Ainsi, si le test de la ligne 9 avait été un test d’égalité sur la valeur 1, comme le branchement aurait été effectué en ligne 8, la ligne 9 n’aurait jamais été exécutée bien que le test soit vérifié.

2 structures de boucleEn Go, il n’existe qu’une seule structure de boucle pouvant

être utilisée de différentes manières en fonction des besoins. Les boucles se feront donc à l’aide d’une instruction for qui admettra plusieurs syntaxes.

2.1 La boucle infinieLa boucle infinie s’écrit de manière très simple en utilisant

un bloc ouvert par une instruction for. Pour sortir de la boucle, il faudra employer une instruction break :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: for {

08: if a == 10 { 09: break10: } else { 11: a++ 12: } 13: }14: fmt.Println("Valeur de a:", a) 15: }

Une variable a est initialisée à 1 en ligne 6, puis la boucle infinie démarre en ligne 7. Si a vaut 10, on sort de la boucle (ligne 9). Sinon, la variable est incrémentée grâce à l’opérateur ++ (équivalent de a = a + 1) en ligne 11. Enfin, en sortie de boucle, la valeur de la variable est affichée (ligne 14).

Si plusieurs blocs sensibles au break sont imbriqués (for ou switch, par exemple), vous pouvez ajouter des étiquettes pour identifier les blocs et indiquer dans le break quelle est la boucle que vous souhaitez terminer.

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: L1: 08: for {09: switch { 10: case a == 5: break L111: default: a = a + 1 12: } 13: fmt.Println("Valeur de a:", a) 14: }15: fmt.Println("Valeur finale de a:", a) 16: }

En ligne 7, nous définissons une étiquette relative au bloc ouvert par le for de la ligne 8. Ainsi, lorsqu’en ligne 10 nous indiquons que nous souhaitons sortir du bloc, nous précisons qu’il s’agit du for (L1) et non du switch. En effet, si nous avions omis de préciser que le break portait sur L1, la commande break de la ligne 10 nous aurait fait sortir du switch et nous serions restés dans la boucle infinie du for avec une variable a ayant pour valeur 5.

L’instruction continue est également disponible : cette instruction permet de sauter une itération tout en restant dans la boucle. Par exemple, nous pouvons créer une boucle qui affiche seulement les entiers pairs compris entre 1 et 10 :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 0 07: for {08: a++

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com14

iNtroductioN & NotioNs de base La syNtaxe de base

09: if a % 2 != 0 { 10: continue11: } else { 12: fmt.Println("Valeur de a:", a) 13: if a == 10 { 14: break15: } 16: } 17: }18: }

La variable de boucle est déclarée à l’extérieur de la boucle, en ligne 6. En ligne 9, nous testons le résultat du reste de la division entière de a par 2 : si a est impair, alors ce résultat est différent de zéro et l’on passe à l’itération suivante grâce à l’instruction continue (ligne 10). Sinon, on affiche la valeur de la variable et lorsque cette dernière vaut 10, on quitte la boucle (ligne 14).

2.2 boucle while ou « tant que »Pour écrire une boucle équivalente à une boucle while,

l’instruction for sera suivie d’une condition : lorsque la condition est invalidée, nous sortons de la boucle.

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: for a < 5 {08: fmt.Println("Valeur de a:", a) 09: a++ 10: }11: fmt.Println("Valeur finale de a:", a) 12: }

Tant que la condition de la ligne 5 est vérifiée, les lignes 8 et 9 seront exécutées. On retrouve bien le fonctionnement de la boucle while traditionnelle et il faut donc faire attention à la modification de la variable de boucle (ici a) pour ne pas rentrer dans une boucle infinie.

2.3 Le for « classique »Il est possible d’écrire en Go un for ayant la même

structure qu’en C. Il s’agit en fait de la syntaxe précédente à laquelle sont ajoutés deux traitements optionnels :

for pré-traitement; condition; post-traitement { // Traitement}

Nous nous trouvons donc en présence d’une écriture classique où il est possible de définir une variable de boucle qui ne sera visible que dans le bloc associé :

01: package main 02: 03: import "fmt" 04: 05: func main() {

06: for a := 1; a <= 5; a++ {07: fmt.Println("Valeur de a:", a) 08: }09: // fmt.Println("Valeur finale de a:", a) 10: }

En ligne 6, la variable de boucle est déclarée dans la phase de pré-traitement, la condition qui doit être vérifiée pour continuer la boucle est a <= 5, et le post-traitement consiste à incrémenter la variable de boucle. La ligne 9 est mise en commentaire, car la variable a n’est plus visible et l’instruction d’affichage provoquerait une erreur. Cette écriture est en fait un raccourci de l’écriture de style while que nous avons vue précédemment, puisque l’on peut également écrire notre code sous la forme :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: a := 1 07: for ; a <= 5; {08: fmt.Println("Valeur de a:", a) 09: a++ 10: }11: }

2.4 Le parcours de tableauxAssociées à l’instruction range, les boucles for peuvent

parcourir des tableaux (au sens large), récupérant indices, clés ou valeurs. Cet aspect de l’instruction for sera traité dans l’article dédié aux tableaux.

3 Même les meilleurs font des erreurs...

L’abomination ultime est malheureusement présente dans Go... Peut-être est-ce dû à son nom, dont la première syllabe est la même que le nom du langage ? Toujours est-il que l’instruction goto existe ! Elle fonctionne comme un goto classique : il faut définir une ou des étiquette(s) et indiquer sur quelle étiquette vous souhaitez effectuer votre embran-chement. Comme je suis certain que vous ne l’utiliserez pas, je ne développerai pas son utilisation...

Par contre, bonne nouvelle : l’opérateur ternaire n’existe pas !

ConclusionL’utilisation des boucles et des tests se trouve simplifiée :

deux instructions pour les tests et une seule pour les boucles. Inutile donc de se creuser la tête pour savoir quelle structure employer : un gain de temps pour le développeur !

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com

Numéros de

GNU/Linux Magazine11

Profitez de nos offres d’abonnement spéciales disponibles au verso !

Les 3 bonnes raisons de vous abonner : Ne manquez plus aucun numéro. Recevez GNU/Linux Magazine chaque mois chez vous ou dans

votre entreprise. Économisez 27,50 €/an ! (soit plus de 3 magazines offerts !)

Tournez svp pour découvrir toutes les offres d’abonnement >>

Abonnez-vous !

Bon d’abonnement à découper et à renvoyer à l’adresse ci-dessous

Vos remarques :

Édité par Les éditions Diamond service des Abonnements B.p. 20142 - 67603 sélestat CedexTél. : + 33 (0) 3 67 10 00 20 Fax : + 33 (0) 3 67 10 00 21

Économisez plus de

*30%* Sur le prix de vente unitaire France Métropolitaine

* OFFRE VALABLE UNIQUEMENT EN FRANCE MéTROPOLITAINE Pour les tarifs hors France Métropolitaine, consultez notre site : www.ed-diamond.com

4 façons de commander facilement : par courrier postal en nous renvoyant le bon ci-dessous par le Web, sur www.ed-diamond.com par téléphone, entre 9h-12h et 14h-18h au 03 67 10 00 20 par fax au 03 67 10 00 21

économie : 27,50 €*

au lieu de 82,50 €* en kiosque

par ABONNEMENT : 55€*

Téléphonez au 03 67 10 00 20ou commandez par le Web

Voici mes coordonnées postales :Société :Nom :Prénom :Adresse :

Code Postal :Ville :Pays :Téléphone :e-mail :

En envoyant ce bon de commande, je reconnais avoir pris connaissance des conditions générales de vente des éditions Diamond à l’adresse internet suivante : www.ed-diamond.com/cgv et reconnais que ces conditions de vente me sont opposables.

Tournez svp pour découvrir toutes les offres d’abonnement

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 17

Les VariabLes

Les DIffÉrenTs TyPes De VarIabLes par Tristan Colombo

L’une des activités principales de la programmation consiste à manipuler des données stockées en mémoire et accessibles sous forme de variables. Encore faut-il savoir ce que peuvent contenir ces variables...

Le langage Go est sensible à la casse. Cela signifie que les identifiants écrits en majuscules

ou en minuscules ne représentent pas le même élément (nom de variable, mot-clé, etc.). Ainsi, maVariable, mavariable et MAVARIABLE représentent trois identi-fiants de variables pouvant coexister.

La déclaration des variables, quant à elle, peut utiliser un modèle de typage dynamique ou statique, comme nous allons le voir dans cet article.

1 Typage dynamique

Nous avons déjà utilisé cette forme de déclaration qui permet de ne pas spéci-fier le type de la variable. Celui-ci sera déduit à partir du type de la donnée qui y est stockée. Attention : contrairement aux langages interprétés, une fois fixé, le type de la variable ne pourra plus changer en fonction du type des données successives qui y seront stockées.

La déclaration se fait en utilisant un opérateur particulier : :=. Pour chan-ger la valeur de la variable, il faudra ensuite utiliser l’opérateur d’affectation classique =. Voici un exemple où nous déclarons et affichons le contenu de deux variables :

01: package main 02: 03: import "fmt" 04:

05: func main() { 06: a := 5 07: b := "GLMF" 08: fmt.Println("Valeur de a:", a) 09: fmt.Println("Valeur de b:", b) 10: }

La variable a déclarée en ligne 6 prend un type entier et la variable b déclarée en ligne 7 prend un type chaîne de caractères. Les valeurs de ces variables sont ensuite affichées en lignes 8 et 9.

Si nous voulons changer la valeur de la variable a, il suffit de lui affecter une nouvelle valeur :

a := 5a = 12

Par contre, si nous changeons de type, le programme ne compilera pas :

a := 5a = "GLMF"

Nous obtiendrons le message d’er-reur suivant :

# variables src/variables/variables.go:7: cannot use "GLMF" (type string) as type int in assignment

Notons enfin la présence d’un mé-canisme particulier, très pratique, qui ne sera pas inconnu des développeurs Python : l’affectation multiple. Vous pouvez en effet chaîner les déclarations en utilisant des virgules :

a, b, c := 5, "GLMF", 10

Cette ligne signifie que la variable a a pour valeur 5, la variable b contient la chaîne de caractères « GLMF » et enfin, que la variable c a pour valeur 10. Mais ce n’est pas là que ce mécanisme révèle toute sa puissance, c’est plutôt lors de permutations. En effet, il faut traditionnellement utiliser une variable intermédiaire pour intervertir les valeurs de deux variables. En Go non optimisé, on ferait :

a := 1b := 2tmp := aa := bb := tmp

En sortie, la valeur qui était contenue dans la variable b est passée dans a et inversement. En utilisant l’affectation multiple, les trois dernières lignes peuvent être résumées en une seule :

a := 1b := 2a, b := b, a

Cette technique fonctionnera égale-ment en typage statique.

2 Typage statique

Le langage Go met à notre disposition de nombreux types. Nous ne verrons dans cette partie que les types de bases, les types plus complexes comme les tableaux seront vus de manière plus approfondie par la suite.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com18

Les VariabLes Les différeNts tyPes de VariabLes

2.1 Les types numériquesGo propose un choix très vaste de types numériques. On trouvera ainsi des

types pour les entiers, les réels et pour les nombres complexes.

2.1.1 entiersLa gestion des entiers se fait de manière très fine. Sur combien de bits doivent-

ils être stockés ? Sont-ils signés ou non ? (Un entier non signé libère le bit qui contenait le signe et permet de stocker de plus gros entiers positifs). Le tableau suivant donne la liste des types disponibles :

Type Plage de valeurs

Entiers non signés uint8 ou byte 0 à 255uint16 0 à 65 535uint32 0 à 4 294 967 295uint64 0 à 18 446 744 073 709 551 615

Entiers signés int8 - 128 à 127int16 - 32768 à 32767int32 ou rune - 2 147 483 648 à 2 147 483 647int64 - 9 223 372 036 854 775 808 à

9 223 372 036 854 775 807

Il existe deux types supplémentaires, int et uint, qui en fonction de l’archi-tecture du microprocesseur prendront respectivement la même taille que int32 et uint32 ou que int64 et uint64.

Mais que se passe-t-il si l’on dépasse la plage autorisée par un type ? Deux cas se posent :

- dépassement lors de la déclaration : le code ne compile pas et un message d’erreur est affiché. Le code suivant illustre ce comportement :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var n int8 = 128 07: 08: fmt.Println("Valeur de n :", n) 09: }

Un entier sur 8 bits (ou int8) peut contenir des valeurs allant de -128 à 127. La déclaration de la ligne 6 dépasse cette plage et lors de la compilation elle provoque une erreur :

# variables src/variables/variables.go:6: constant 128 overflows int8

- dépassement lors de calculs : les calculs sont effectués et les bits sortant de la plage ne sont pas utilisés. Modifions l’exemple précédent :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var n int8 = 127 07: 08: n++ 09: fmt.Println("Valeur de n :", n) 10: }

En ligne 6, cette fois, la variable est correctement déclarée avec une valeur valide pour le type int8. Mais il s’agit de la valeur maximale pour ce type. Donc, lorsque l’on ajoute 1 en ligne 8, nous sortons de la plage autorisée... et pourtant, ce code peut être compilé ! Où est le problème alors ? Le problème vient de la valeur affichée en sortie :

Valeur de n : -128

Donc 127 + 1 = -128 ? Oui ! N’oubliez pas que nous sommes en représentation binaire et que l’entier 127 est codé sur 8 bits, soit 01111111 (le premier bit indiquant le signe, 0 pour positif et 1 pour négatif). Vérifions l’exactitude de la représentation : 0 pour un signe positif, puis 1x2⁶ + 1x2⁵ + 1x2⁴ + 1x2³ + 1x2² + 1x2¹ + 1x2⁰ = 64 + 32 + 16 + 8 + 4 + 2 + 1 = 127. Si nous ajoutons 1 à cet entier, nous allons en fait ajouter 00000001 à 01111111. Comme en binaire 1 + 1 = 10, 01111111 + 00000001 = 10000000. Le résultat nous indique donc une valeur de 0 avec un signe négatif. Le calcul d’un entier signé négatif sur n bits s’effectue en calculant l’entier positif sur les n-1 bits qui ne sont pas des bits de signe, puis en retranchant 2n-1. Dans notre cas 27 = 128 donc 10000000 représente 0 - 128 soit -128. Voilà l’explication de ce résultat logique, mais ne corres-pondant pas forcément aux attentes...

2.1.2 réelsPour les réels, il y a beaucoup moins

de types disponibles : ce sera soit float32 pour un codage sur 32 bits, soit float64 pour un codage sur 64 bits.

J’attire ici votre attention sur le pro-blème classique de représentation des réels en machine [1]. Le code suivant, qui effectue un calcul très simple (5.2 x 3), ne renvoie pas le bon résultat :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: const n float32 = 5.2 07: 08: fmt.Println("Valeur de n :", 3*n) 09: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 19

Les VariabLes Les différeNts tyPes de VariabLes

Le résultat produit par la ligne 8 n’est pas 15.6 comme on aurait pu s’y attendre, mais 15.599999.

En machine, les réels sont représentés par des nombres à virgule flottante (la longueur de la partie fractionnaire n’est pas fixée). Ces nombres sont encore appelés « flottants », ou float en anglais. Ils permettent de coder à la fois des nombres très petits et des nombres très grands. Ils sont déterminés par la formule suivante :

x = (-1)s x m x be

Dans cette formule, on a :

- s le signe du nombre, codé sur un bit : si s=0 alors (-1)0=1 et le nombre sera positif ; si s=1 alors (-1)1=-1 et le nombre sera négatif ;

- m la mantisse : il s’agit du nombre « définissant » le réel (comme dans l’écriture scientifique de 12.5=1.25x101, 1.25 représente la mantisse). La mantisse sera codée sur 23 bits pour un codage float32 et sera codée sur 52 bits pour un codage float64 (à condition que le microprocesseur possède une architecture 64 bits). On considère que m sera toujours supérieur à 1 et inférieur à 2. Cette considération nous permet de dire que la mantisse sera toujours de la forme 1.xxx et donc de définir une mantisse normalisée : on omet le premier bit qui sera toujours présent, ce qui permet de gagner un bit pour la précision du nombre. En clair, en codage float32, la mantisse du nombre 1.01101 sera m=011010...0 (la fin du nombre est complétée par des 0 pour obtenir au total 23 bits).

- b la base : dans le cas d’une re-présentation en machine, nous travaillerons forcément en binaire, donc b=2 ;

- e l’exposant permettant de faire varier la place de la virgule. Pour un codage float32, il est codé sur 8 bits et pour un codage float64, il est codé sur 11 bits. Cet exposant

sera biaisé à 127 en float32 et sera biaisé à 1023 en float64. Pourquoi ? En prenant le codage float32, sur 8 bits on peut coder les nombres de 0 à 256, et comme l’exposant peut être négatif, en ajoutant 127 à sa valeur originelle, on peut coder les nombres de -127 à 127. Exemple : mon exposant originel vaut -12, si je le biaise à 127, j’obtiens 127-12 = 115 et je peux représenter ce nombre sur les 8 bits qui me sont alloués.

Pour bien comprendre le fonction-nement de ce codage, prenons un réel codé en machine et traduisons-le :

n = 0 011111110 01100110011001100110011

Dans ce nombre, on identifie :- le signe s = 0 ;- l’exposant biaisé e = 01111110 ;- la mantisse m = 01100110011001100110011.

Commençons la traduction :- s = 0 donc le nombre est positif.- e = 01111110 en binaire et donc e = 126 en décimal (1x21+1x22+...+1x26). Comme e est biaisé à 127, nous obtenons : e = 126-127 = -1.

- Pour la conversion de la mantisse, il ne faut pas oublier le bit caché : m = 1+1x2-2+1x2-3+...+1x2-23. Donc m = 1.39999997616.

Notre nombre est donc égal à n = 1.39999997616 x 2-1 = 0.699999988079. Or, au départ, le nombre n devait représenter la valeur 0.7. Nous nous retrouvons donc face à une petite erreur d’arrondi. Mais la multiplication de ces erreurs peut aboutir à des résultats complètement aberrants ! Et suivant le niveau de précision exigé, une seule « petite » erreur d’arrondi produira de toute façon un résultat faux.

Ce mécanisme n’est pas spécifique à Go, mais se retrouve dans tous les langages. Il faut donc y faire très atten-tion... En Go, le paquetage permettant de conserver le plus longtemps possible des valeurs exactes pour les calculs se nomme math/big. Vous pourrez définir

des nombres rationnels, effectuer tous vos calculs, puis au dernier moment convertir votre résultat en réel. Voici un exemple avec le problème précédent :

01: package main 02: 03: import ( 04: "fmt" 05: "math/big" 06: ) 07: 08: func main() { 09: a := big.NewRat(52, 10) 10: b := big.NewRat(3, 1) 11: c := big.NewRat(0, 1) 12: 13: fmt.Println(c.Mul(a, b).FloatString(2)) 14: }

On utilise donc ici le paquetage math/big comme le montre la ligne 5. Dans les lignes 9 à 11, nous définissons trois variables rationnelles grâce au constructeur big.NewRat() qui prend en paramètres un numérateur et un dénominateur. Ainsi, la variable a a pour valeur 52/10. Il s’agit donc d’une valeur exacte. La variable c, qui est initialisée à 0 en ligne 11, ne servira qu’à contenir le résultat de la multiplication.

En effet, la méthode Mul() employée en ligne 13 pour réaliser la multiplication de a et de b renvoie un objet de type rationnel. Pour afficher le résultat sous forme décimale, nous appliquons à ce résultat la méthode FloatString() qui transforme un rationnel en flottant en utilisant comme précision (nombre de chiffres après la virgule) le nombre transmis en paramètre. Cette dernière ligne aurait pu être écrite de manière moins compacte :

13: c.Mul(a, b) 14: fmt.Println(c.FloatString(2))

2.1.3 nombres complexesIl existe deux types permettant de

manipuler des nombres complexes : complex64 où la partie réelle et la partie imaginaire sont codées sous forme de float32 et complex128 où la partie réelle et la partie imaginaire sont codées sous forme de float64. Voici un exemple d’utilisation des nombres complexes :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com20

Les VariabLes Les différeNts tyPes de VariabLes

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var z1 complex64 = complex(-1, 2) 07: var z2 complex64 = complex(5, -3)08: fmt.Println(z1 + z2) 09: }

2.1.4 ConversionPour convertir une variable d’un type en un autre type,

il suffit d’utiliser une « fonction » de typage : le nom du type attendu, suivi de la variable à convertir entre paren-thèses. Si les types sont compatibles, la conversion sera effectuée. Par exemple, il est possible de convertir une variable réelle en entier :

var n float32 = 3.141159fmt.Println("int n=", int(n)) // Résultat : 3

Par contre, une constante réelle ne pourra pas être convertie en chaîne de caractères :

const n float32 = 3.141159fmt.Println("string n=", string(n))

Nous obtiendrons ici un message d’erreur à la compilation :

# variables src/variables/variables.go:8: cannot convert n to type string src/variables/variables.go:8: cannot convert n (type float32) to type string

De plus, la conversion ne donnera pas toujours le résultat attendu en première intention... Ainsi, la conversion d’un entier en chaîne de caractères ne donnera pas cet entier sous forme de caractères mais le caractère correspondant à cet entier dans la table ASCII :

const n int8 = 65fmt.Println("string n=", string(n))

La dernière ligne affichera le caractère correspondant au code 65 dans la table ASCII, soit la lettre A. Les conversions sont donc à manipuler avec beaucoup de précautions...

2.1.5 Travailler avec des nombresLe paquetage mathématique portant le nom de math

fournit les outils classiques permettant d’effectuer des calculs : Sin(), Cos(), Pow(), Pi, etc. Voici un petit exemple d’application :

01: package main 02: 03: import ( 04: "fmt" 05: "math" 06: ) 07: 08: func main() { 09: fmt.Println("cos(¶/2) =", math.Sin(math.Pi/2)) 10: }

2.2 Les chaînes de caractèresLes chaînes de caractères font partie des types complexes

au sujet desquels un article complet est dédié. En première approximation, il s’agit d’une suite de caractères définie par le mot-clé string. C’est le type que nous avons utilisé le plus souvent jusqu’à maintenant :

var msg string = "GLMF HS GO"fmt.Println(msg)

2.3 Les booléensLe type bool représente une variable booléenne pouvant

avoir pour valeur true ou false. Les opérateurs booléens sont les mêmes qu’en C : ! pour NOT, && pour AND et || pour OR. La plupart du temps, ces variables sont utilisées pour effectuer des tests :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var vrai bool = true07: var faux bool = false08: 09: fmt.Println("Valeur du test : ", vrai && faux) 10: }

3 ConstantesLa déclaration de constantes se fait à l’aide d’un mot-clé

particulier : const. Une constante peut être typée dynami-quement ou statiquement, mais contrairement aux variables, l’opérateur de déclaration sera toujours =. Ainsi, pour déclarer une constante de type entier, deux écritures seront possibles :

const max = 512const maximum int = 512

Pour déclarer de multiples constantes, il est inutile de ré-péter le mot-clé const : la même syntaxe que pour l’import de plusieurs paquetages peut être employée. Prenons par exemple la déclaration de constantes indiquant des codes couleur :

const black = 30const blue = 34const yellow = 33

Ces lignes peuvent être écrites de manière plus élégante :

const ( black = 30 blue = 34 yellow = 33)

Si vous devez définir des constantes entières dont les valeurs se suivent, vous pourrez utiliser le mot-clé iota. Ce mot-clé ne s’utilise qu’avec les constantes ; sa première valeur est 0

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 21

Les VariabLes Les différeNts tyPes de VariabLes

et elle est incrémentée pour chaque utilisation successive (si une déclaration intermédiaire n’utilise pas iota, sa valeur sera réinitialisée à 0). Voici quelques exemples illustrant ce comportement. Tout d’abord, déclarons trois constantes ayant pour valeur 0, 1 et 2 :

const black = iotaconst blue = iotaconst yellow = iota

Nous obtenons bien le résultat attendu. Par contre, si la constante blue n’utilise plus iota, le résultat sera différent :

const black = iotaconst blue = 1const yellow = iota

Ici, la constante yellow aura pour valeur 0 puisque iota aura été réini-tialisé. Ce mot-clé prendra tout son intérêt dans une déclaration multiple où il n’aura pas à être répété :

const ( black = iota blue yellow)

Ici, black , blue et yellow ont respectivement pour valeur 0, 1, et 2.

Bien entendu, s’agissant de constantes, vous ne pourrez plus modifier la valeur d’une variable déclarée avec const.

4 Variable « blanche » _

L’identifiant _, appelé blank identifier, est réservé pour être utilisé en tant que variable « poubelle » : si une fonction renvoie une valeur que vous ne souhaitez pas conserver, vous pourrez utiliser cette variable pour ne pas conserver la valeur. Par exemple, la fonction fmt.Println() que nous utilisons dans nos exemples retourne deux valeurs : le nombre d’octets affichés (le nombre de lettres de la chaîne de caractères, auquel il faut ajouter le saut de ligne), et éventuellement un message d’erreur. Pour récupérer ces deux valeurs, nous pouvons écrire :

nb_car, err := fmt.Println("GLMF")

Si vous ne souhaitez récupérer que le nombre de caractères, inutile d’encombrer la mémoire avec une variable contenant les erreurs... Mais impossible d’écrire nb_car := fmt.Println("GLMF"), car la fonction renvoie deux valeurs. Que faire alors ? Il suffit d’employer l’identifiant _ :

nb_car, _ := fmt.Println("GLMF")

Ainsi, plus d’erreur, et l’occupation mémoire est optimisée...

5 Créer ses propres types

La déclaration d’une structure com-posée se fait à l’aide de type...struct. On retrouve ici l’architecture de la décla-ration en langage C, à quelques détails de syntaxe près. L’accès aux éléments de la structure s’effectuera à l’aide de l’opérateur .. Voici un exemple d’appli-cation où nous allons définir et utiliser un point en coordonnées cartésiennes :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: type point struct { 07: x float32 08: y float32 09: }10: 11: var A point = point{1, 2} 12: fmt.Println("Point A : (", A.x, ",", A.y, ")") 13: }

Dans les lignes 6 à 9, la structure définissant un point indique qu’il y aura deux champs de type réel (codés sur 32 bits) notés x et y. La déclaration d’une variable de type point en ligne 11 est faite de manière statique. Vous noterez que l’affectation ne se fait plus de ma-nière traditionnelle : il faut préciser le nom de la structure, puis indiquer les valeurs des champs entre accolades. Enfin, pour l’utilisation en ligne 12, A.x fait référence à la valeur 1 et A.y fait référence à la valeur 2.

La déclaration en typage dynamique se fera de manière classique :

A := point{1, 2}

Les structures peuvent bien sûr être enfouies dans d’autres structures. Voici un exemple avec une structure triangle comportant trois points :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: type point struct { 07: x float32 08: y float32 09: } 10: 11: type triangle struct { 12: a, b, c point 13: }14: 15: triangle_1 := triangle{point{1, 2}, point{5, 12}, point{4, -6}} 16: fmt.Println("Triangle : ", triangle_1) 17: }

La structure triangle des lignes 11 à 13 utilise trois champs de type point (ligne 12). Lors de la déclaration de la variable triangle_1 en ligne 15, nous devons énoncer l’ensemble des structures : triangle contient trois champs point et ce sont ces éléments que l’on initialise.

ConclusionLa gestion de l’espace mémoire occupé

par les variables peut être effectuée de manière très fine en Go. On retrouve bien le compromis annoncé entre simplicité d’écriture pour le développeur, avec la possibilité d’utiliser un typage dynamique, et optimisation du code avec cette gestion poussée de l’occupation mémoire des variables. Nous n’avons traité dans cet article que des types de base et nous avons notamment survolé les chaînes de caractères. Mais celles-ci vont être abordées dans l’article suivant...

Référence[1] Tristan (COLOMBO), « Au-delà

des réels, l’aventure continue... », GNU/Linux Magazine n°113, p. 60 à 63, février 2009.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com22

Les VariabLes tout saVoir sur Les chaîNes de caractères

ToUT saVoIr sUr Les CHaînes De CaraCTères

par Tristan Colombo

Nous utilisons constamment des chaînes de caractères, que ce soit pour afficher des messages ou rechercher des informations dans un fichier texte. Il est donc important de maîtriser parfaitement la manipulation de ces objets.

Qu’appelle-t-on chaîne de caractères en Go ? Il s’agit d’une séquence non modi-

fiable d’octets. Par « non modifiable », on entend que, comme en Python, il est impossible de remplacer un caractère par un autre à l’intérieur d’une chaîne constituée. J’ai déjà dit qu’en Go les ca-ractères suivaient le standard Unicode, mais je n’ai pas précisé de quel encodage il s’agissait. C’est bien entendu l’UTF-8 qui est utilisé... En effet, rappelez-vous que deux des créateurs du langage, Rob Pike et Ken Thompson, sont également créateurs de l’UTF-8... Ils pouvaient difficilement choisir un autre encodage ! Chaque caractère est défini par un code de la forme \uhhhh où h représente un nombre en hexadécimal (de 0 à F). En mémoire, les caractères Unicode sont stockés dans l’espace occupé par une rune (rune est un alias du type int32), donc sur 32 bits. L’avantage de Go est que les caractères ASCII ne seront stockés que sur 8 bits. En comparaison, Java ou Python utilisent au moins 16 bits.

Pour créer une chaîne de caractères, nous utiliserons le type string et nous encadrerons les caractères par des guillemets :

var s string = "GLMF"s2 := "HS GO"

Pour « échapper » ou protéger des caractères, on emploie le caractère anti-slash. Ainsi, pour stocker des guillemets à l’intérieur d’une chaîne, nous ferons :

var s string = "Il dit : \"Salut à tous!\""

On retrouvera toutes les séquences d’échappement classiques avec \\ pour l’anti-slash, \t pour la tabulation, \n pour le saut à la ligne, \r pour le re-tour chariot, \b pour l’effacement, etc. L’exemple suivant montre une utilisation de \b pour corriger un affichage :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var s string = "Downloading : 0%" 07: fmt.Print(s) 08: fmt.Print("\b\b1%") 09: }

La fonction fmt.Print() utilisée dans les lignes 7 et 8 a le même com-portement que fmt.Println(), mais sans saut à la ligne automatique en fin d’affichage. Nous affichons ici le message « Downloading : 0 % », qui sera ensuite corrigé en « Downloading : 1 % ». Si des opérations lentes précèdent la ligne 8 et que nous paramétrons la valeur affichée, nous obtenons un mécanisme de barre de progression.

Pour afficher un caractère en UTF-8, si vous ne voulez pas l’inscrire direc-tement dans votre code, vous pouvez utiliser une séquence \uhhhh. L’exemple suivant affiche la lettre grecque pi :

var s string = "Pi : \u03C0"

Les anti-quotes permettent de définir une chaîne brute : tous les caractères

seront affichés et il n’y aura aucune interprétation d’un codage quelconque. Exemple :

var s string = `\"Alpha : \u03C0`

Cette chaîne contient exactement les caractères indiqués : \"Alpha : \u03C0.

1 opérations de base

Dans cette partie, je vous présente les fonctions permettant de manipuler les chaînes de caractères.

1.1 ConcaténationLa concaténation, opération per-

mettant de regrouper deux chaînes de caractères en une seule, se fait à l’aide de l’opérateur + :

var s1 string = "Hello"var s2 string = " GLMF!"fmt.Print(s1 + s2)

Vous avez également la possibilité de stocker cette concaténation en tant que nouvelle variable :

var s3 string = s1 + s2

Sinon, vous pouvez ajouter la chaîne s2 à la fin de la chaîne s1 (ce qui revient à une ré-affectation et non une modifi-cation). Les deux lignes suivantes sont équivalentes :

s1 = s1 + s2s1 += s2

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 23

Les VariabLes tout saVoir sur Les chaîNes de caractères

1.2 accès par indiceDans une chaîne de caractères, chaque

caractère se trouve à une position pré-cise repérée par un indice. Le premier caractère se trouve à l’indice 0 et ainsi de suite. Pour accéder à un caractère particulier, on utilise les crochets. At-tention : cette technique permet d’aller lire en mémoire un octet (équivalent à uint8) et renvoie donc le code ASCII du caractère ou une partie du code UTF-8 si ce dernier ne fait pas partie de la table ASCII ! L’exemple suivant affiche le code ASCII du premier caractère de la chaîne (A dont le code est 65) :

var s string = "ABCD"fmt.Print(s[0])

Pour afficher la lettre, il faudra utiliser la conversion en string() vue dans le précédent article :

fmt.Print(string(s[0]))

Dans le cas où le premier caractère possède un code UTF-8, nous n’obtiendrons qu’une partie du code et la conversion en chaîne de caractères n’affichera pas le bon caractère :

var s string = "\u03C0BCD"fmt.Print(string(s[0]))

1.3 TailleLa taille d’une chaîne de caractères

est donnée par la fonction len(). Ainsi, la chaîne « GLMF » a pour taille 4 et ses indices vont de 0 à 3. Grâce à la taille, nous pouvons par exemple parcourir une chaîne et l’afficher caractère à caractère :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var s string = "GLMF" 07: var size int = len(s) 08: for i := 0; i < size; i++ { 09: fmt.Println("==> " + string(s[i])) 10: } 11: }

Petit rappel : il vaut mieux stocker la taille de la chaîne dans une variable

(ligne 7) plutôt que de la recalculer à chaque itération en écrivant la condi-tion d’arrêt de la boucle de la ligne 8 sous la forme i < len(s). Ceci permet d’économiser du temps de calcul.

1.4 ConversionsNous avons vu que la conversion

en chaîne de caractères était possible à l’aide de string(), mais ce n’est pas le seul mécanisme. En effet, si vous souhaitez obtenir un entier sous forme de chaîne, string() vous renverra le caractère correspondant au code ASCII. Ainsi, string(65) ne renverra pas « 65 » mais « A ». Il faut alors utiliser un autre mécanisme. Si vous souhaitez simple-ment afficher la chaîne de caractères, il faudra utiliser fmt.Sprint(). Ainsi, en reprenant l’exemple précédent, si nous souhaitons afficher pour chaque caractère la position de celui-ci dans la chaîne, il faudra remplacer la ligne 9 par :

09: fmt.Println("caractère n." + fmt.Sprint(i+1) + " " + string(s[i]))

Le paquetage strconv fournit une fonction Itoa() permettant de conver-tir un entier sous forme de chaîne et renvoyant éventuellement une erreur. Nous reviendrons sur ce paquetage dans la section « Paquetages dignes d’intérêt ». En utilisant cette fonction, la ligne 9 de notre exemple devient :

09: fmt.Println("caractère n." + strconv.Itoa(i+1) + " " + string(s[i]))

Il ne faudra surtout pas oublier d’importer le paquetage strconv pour pouvoir compiler...

1.5 Comparaison de chaînes de caractères

Pour comparer des chaînes de carac-tères, vous pourrez utiliser les opérateurs de comparaison classiques : <, <=, ==, !=, >=, et >. Les opérateurs < et > peuvent se lire : « précède (respectivement suit) dans l’ordre alphanumérique ». Toutes les comparaisons renvoient un booléen.

L’encodage en UTF-8 peut poser pro-blème... En effet, un caractère accentué

peut avoir plusieurs codes (par exemple, on peut avoir le caractère « é » en ASCII ou un « e » sur lequel on ajoute un accent « ‘ »). La comparaison affichera alors un résultat qui semblera incompréhensible, puisque deux caractères qui sont à nos yeux identiques seront considérés comme différents. Il en va de même avec le classement par ordre alphabétique qui va dépendre de la langue utilisée. Par exemple, en danois, le « ø » ne se trouve pas après le « o », mais après le « z ». Suivant les tests que vous devez effectuer, gardez ces considérations à l’esprit pour ne pas vous trouver face à des bugs impossibles à corriger...

2 Le découpage en tranches ou slicing

Go supporte le découpage en tranches (ou slicing) en s’inspirant de Python. Pour les pythonistes, cela ne présentera donc pas une nouveauté, mais pour les autres je vais m’y attarder un peu pour en expliquer le fonctionnement.

Comme son nom l’indique, le décou-page en tranches permet de sélectionner simplement une tranche ou portion de chaîne de caractères. Pour ce faire, nous indiquerons l’indice de départ et l’indice d’arrivée (non-compris) de notre tranche à l’aide d’une syntaxe du type : chaine[départ:arrivée]. Pour les habitués de Python, inutile de chercher à régler le pas, ce paramètre n’est pas présent en Go.

Prenons une chaîne de caractères s = "Linux Magazine" : les caractères seront indicés de 0 à len(s)-1 = 13. Si nous voulons afficher seulement le mot « Linux », il va nous falloir découper la chaîne depuis le premier caractère situé à l’indice 0 (le « L »), jusqu’à celui situé à l’indice 4 (le « x »). Comme la syntaxe demande d’indiquer le dernier indice non-compris, nous utiliserons la syntaxe s[0:5] (Fig. 1, page suivante). Le code de cet exemple est le suivant :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com24

Les VariabLes tout saVoir sur Les chaîNes de caractères

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var s string = "Linux Magazine" 07: 08: fmt.Println(s[0:5]) 09: }

Fig. 1 : Découpage en tranches de la chaîne « Linux Magazine »

Notez que grâce à cette écriture vous pouvez aussi récupérer un seul caractère qui sera considéré en tant que tel et non plus en tant que caractère ASCII. Ainsi, s[0] renverra le code ASCII de la lettre « L » (soit 76), mais s[0:1] renverra la lettre « L ».

Cette technique fonctionne bien pour une chaîne écrite entièrement avec des caractères ASCII... Qu’en est-il si nous ajoutons un caractère en UTF-8 ? Reprenons l’exemple précédent et in-sérons entre « Linux » et « Magazine » une flèche à la place de l’espace :

var s string = "Linux\u21D2Magazine"

Pour commencer, nous pouvons consta-ter que la taille de la chaîne passe de 14 à 16 caractères. Ça s’annonce plutôt mal. Rappelez-vous qu’en utilisant les indices simples, nous ne récupérions que la moitié du codage d’un caractère en UTF-8 : s[5] ne contient pas la flèche ! De la même manière, s[5:6] ne donnera que la moitié du code du caractère et nous obtiendrons donc un caractère différent de celui attendu. Par contre, s[5:7] renverra le bon caractère (et non deux comme nous aurions pu nous y attendre). Ce problème est illustré en figure 2.

Fig. 2 : Problème du découpage en tranches avec les caractères UTF-8

Pour résoudre ce problème, il existe une solution faisant appel aux tableaux (que nous verrons plus tard en détail). Il s’agit de convertir la chaîne de caractères en tableau de rune (équivalent à int32) : tous les caractères occuperont alors le même espace en mémoire et l’accès direct par indice sera facilité. Pour utiliser le résultat en tant que chaîne de caractères, il faudra à nouveau convertir celui-ci :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var s string = "Linux\u21D2Magazine" 07: chars := []rune(s)08: 09: fmt.Print(string(chars[5:6]))10: }

En ligne 7, nous effectuons la conversion de la chaîne s en tableau de rune. En ligne 9, nous récupérons la tranche 5:6 (donc le caractère dont l’indice vaut 5) et nous le convertissons en chaîne de caractères pour l’afficher.

Pour finir sur le slicing, il faut encore savoir que les indices de début et de fin peuvent être omis : les valeurs par défaut seront alors le début et la fin de la chaîne. En utilisant notre chaîne d’exemple s, s[:5] correspond à « Linux », s[6:] correspond à « Magazine » et s[:] correspond à « Linux Magazine ».

3 Le formatage avec le paquetage fmtLe paquetage fmt propose de nombreuses fonctions d’affichage et de saisie

de données. Nous avons déjà vu Print() et Println(). Il est possible d’utiliser une troisième fonction, Printf(), qui est l ‘équivalent du printf() du C et ac-cepte comme arguments une chaîne de formatage suivie d’une liste de valeurs. La chaîne de formatage peut être vue comme un texte à trous, où les trous sont indiqués par des termes spéciaux. Une liste des termes les plus courants est donnée dans le tableau ci-dessous :

Terme Description

%c Un caractère donné sous la forme d’un entier uint8%s Une chaîne de caractères%d Un entier (en base 10)%f Un réel. L’utilisation du point et d’un entier permet de

spécifier le nombre de chiffres à afficher après la virgule. %.2f indique deux chiffres après la virgule.

%p L’adresse d’un pointeur en hexadécimal%t Un booléen (true ou false)%% Affiche le caractère %

Voici un exemple d’utilisation :

var n float32 = 93.4fmt.Printf("Pourcentage %s : %.2f%%\n", "calculé", n)

Nous avons défini ici deux « trous » : une chaîne de caractères %s qui sera remplacée par « calculé » et un réel affiché avec une précision de deux chiffres après la virgule %.2f. La chaîne résultante sera : « Pourcentage calculé : 93.40% ».

Le même mécanisme existe pour stocker la chaîne formatée dans une variable. Il s’agit de la fonction Sprintf() :

var s string = fmt.Sprintf("Pourcentage %s : %.2f%%\n", "calculé", n)

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 25

Les VariabLes tout saVoir sur Les chaîNes de caractères

La fonction Errorf() admet les mêmes paramètres que Printf() et renvoie un message d’erreur (qui ne sera donc pas affiché, à moins de le demander explicitement en récupérant la valeur et en appelant Println()).

Enfin, pour permettre à l’utilisateur de saisir des données, vous allez pouvoir utiliser la fonction Scanf(), équivalent du scanf() du C. Il faudra indiquer ici les adresses des variables dans lesquelles stocker les données, exactement de la même manière qu’en C en préfixant le nom de la variable par un caractère &. Il est dommage que cette écriture n’ait pas été elle aussi simplifiée... Je préfère l’approche de Python, où l’on récupère directement une chaîne de caractères que l’on traite par la suite en fonction des données attendues. En Go, pour demander à l’utilisateur de saisir un entier et une chaîne, il faudra écrire :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var i int 07: var s string 08: 09: fmt.Print("Saisissez un entier suivi d’une chaîne de caractères : ") 10: fmt.Scanf("%d %s", &i, &s)11: fmt.Println("***" + fmt.Sprint(i) + "***") 12: fmt.Println("***" + s + "***") 13: }

Le caractère de séparation lors de la saisie est l’espace. C’est la ligne 10 qui spécifie le format attendu pour la saisie et qui indique que les données doivent être stockées dans les variables i et s. Les lignes 11 et 12 permettent de montrer que seuls les caractères attendus sont récupérés.

4 Plus de fonctions avec le paquetage strings

Nous avons vu jusqu’à présent des outils permettant d’ef-fectuer tous les traitements de base sur les chaînes de carac-tères... Mais d’autres opérations sont également essentielles.

Pour séparer les éléments d’une chaîne de caractères sui-vant un caractère ou une chaîne de caractères spéciale, nous utiliserons Split(). Cette fonction renverra un tableau (voir l’article sur les tableaux) de chaînes de caractères :

var s string = "GLMF|LP|LPE|MISC"tab := s.Split("|")

tab[0] contiendra « GLMF », tab[1] contiendra « LP », etc.

La fonction inverse de Split() consistant à joindre plusieurs chaînes et à intercaler une chaîne entre elles s’appelle Join() :

fmt.Println(strings.Join(tab, "--"))

La ligne précédente affichera « GLMF--LP--LPE--MISC ».

Dans la catégorie des tests, on trouve EqualFolds() permettant de comparer deux chaînes sans activer la sen-sibilité à la casse et Countains(), qui teste si une chaîne est une sous-chaîne d’une autre chaîne de caractères. Les lignes suivantes permettent ainsi d’afficher le résultat de la comparaison de « GLMF » avec « Glmf » et de vérifier si « LM » est une sous-chaîne de « GLMF » :

fmt.Printf("%t\n", strings.EqualFold("GLMF", "Glmf"))fmt.Printf("%t\n", strings.Contains("GLMF", "LM"))

La fonction Repeat() permet de concaténer n fois une chaîne de caractères avec elle-même. Ainsi, sans cette fonction, nous aurions pu répéter les lignes suivantes pour obtenir « GLMF GLMF GLMF GLMF » :

var s string = "GLMF "s += "GLMF "s += "GLMF "s += "GLMF "

Mais avec Repeat(), ce sera beaucoup plus joli :

var s string = strings.Repeat("GLMF ", 4)

Il est courant de vouloir modifier la casse d’une chaîne de caractères. Pour cela, nous disposons de deux fonctions : ToLower() pour passer la chaîne en caractères minuscules, et ToUpper() pour passer la chaîne en caractères majuscules :

var s string = "Ceci est une chaîne de test"fmt.Println(strings.ToLower(s)) // ceci est une chaîne de testfmt.Println(strings.ToUpper(s)) // CECI EST UNE CHAÎNE DE TEST

Pour finir, les fonctions de la famille Trim() seront très efficaces pour nettoyer une chaîne : Trim() va supprimer du début et de la fin de la chaîne les caractères indiqués dans une seconde chaîne, TrimLeft() et TrimRight() vont effectuer le même traitement que Trim(), mais respective-ment seulement au début et à la fin de la chaîne, et enfin TrimSpace() va supprimer tous les caractères d’espacement (espace, tabulation, saut à la ligne, etc.) en début et en fin de chaîne. Voici un exemple d’application de Trim() :

var s string = "ceci est une chaîne de test"fmt.Println(strings.Trim(s, "ceidts "))

Tous les caractères c, e, i, d, t, s, ou espaces présents en début ou en fin de chaîne seront supprimés. On obtient alors « une chaîn ».

5 Manipuler les expressions régulières

La manipulation la plus fine que nous puissions avoir sur les chaînes de caractères se fait à l’aide des expres-sions régulières. Le paquetage Regexp fournit tous les outils pour travailler avec cette technique. Go acceptant

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com26

Les VariabLes tout saVoir sur Les chaîNes de caractères

les expressions régulières standards, je ne reviendrai pas sur la création de ces expressions. Le moteur utilisé dans cette implémentation (moteur RE2) garantit un temps d’exécution linéaire.

On retrouvera dans ce paquetage toutes les fonc-tions classiquement liées aux expressions régulières : recherche et remplacement de motifs. Ces fonctions devront s’appliquer sur un objet de type « expression régulière ». Pour le créer, nous utiliserons le constructeur MustCompile() qui prend en paramètre une chaîne brute (pour pouvoir conserver les anti-slashes dans les motifs). Voici un exemple recherchant des données dans une chaîne et modifiant une partie de cette dernière :

01: package main 02: 03: import ( 04: "fmt" 05: "regexp" 06: ) 07: 08: func main() { 09: var s string = "GLMF n.63" 10: er := regexp.MustCompile(`^(GLMF n\.)(\d+)$`) 11: result := er.ReplaceAllString(s, "${1}64")12: 13: fmt.Println("Motif de recherche : " + er.String()) 14: fmt.Println("Chaîne : " + s) 15: fmt.Println("Résultat : " + result) 16: }

La ligne 10 permet de définir l’expression régulière. Nous recherchons ici une chaîne ne contenant que « GLMF n. » suivi d’un nombre. Les caractères ̂ et $ indiquent un début et une fin de ligne (donc rien d’autre que cette chaîne). La ligne 11 remplace la chaîne s par le premier motif trouvé (${1}) concaténé à la valeur 64. À ce niveau, ${1} contient « GLMF n. » et ${2} contient « 63 ». Si l’expression régulière n’avait pas trouvé de concordance, la variable result aurait pris pour valeur s. Enfin, la méthode String() employée en ligne 13 permet d’afficher l’expression régulière er sous forme de chaîne de caractères.

Pour une meilleure lisibilité des motifs recherchés, vous avez la possibilité de les nommer en utilisant la syntaxe (?P<nom>...). Dans notre exemple, nous aurions ainsi pu écrire les lignes 10 et 11 :

10: er := regexp.MustCompile(`^(?P<mag>GLMF n\.)(?P<num>\d+)$`) 11: result := er.ReplaceAllString(s, "${mag}64")

Pour la liste complète des fonctions de ce paquetage, je vous renvoie à la documentation sur http://golang.org/pkg/regexp/. Ceci me permet de parler de la documenta-tion des paquetages Go. D’un point de vue technique, vous trouverez simplement les en-têtes des fonctions mises à disposition. D’un point de vue pratique, on se trouve à des années-lumières de la documentation Python avec ses multiples exemples... Il faut déjà avoir une idée très

précise du fonctionnement des différents objets pour que cette documentation puisse être utile ! En ce qui concerne les expressions régulières, en vous aidant de l’exemple pré-cédent, vous devriez pouvoir utiliser les diverses fonctions proposées.

6 Paquetages dignes d’intérêt

Dans cette section, je vous présenterai rapidement quelques paquetages supplémentaires permettant de travailler avec les chaînes de caractères.

6.1 utf8Le paquetage utf8 fournit des outils spécifiques à la ma-

nipulation de chaînes encodées en UTF-8 : décodage de code UTF-8, encodage de chaîne, vérification de validité de chaîne UTF-8, etc. Pour importer ce paquetage il faudra taper :

import "unicode/utf8"

6.2 unicodeLe paquetage unicode est plus généraliste que le précé-

dent et permet de vérifier si un caractère Unicode remplit certains critères : est-ce un caractère de contrôle, une lettre majuscule ou minuscule, un caractère de ponctuation, etc. ? L’import se fait par :

import "unicode"

6.3 strconvNous avons déjà évoqué ce paquetage précédemment. Il

contient de nombreuses fonctions de conversion d’un string vers un autre type et inversement. La série des fonctions Parse est particulièrement utile. Par exemple, ParseBool() permet de tester une chaîne de caractères et si cette dernière est égale à « 1 », « t », « T », « true », « True », ou « TRUE » renvoie true. Les valeurs acceptées pour false sont : « 0 », « f », « F », « false », « False » ou « FALSE ». Dans tous les autres cas, la fonction renverra en second résultat un message d’erreur.

ConclusionVous avez pu voir tout au long de cet article qu’il fallait être

particulièrement attentif à la manipulation des caractères à l’intérieur d’une chaîne. Le fait d’utiliser un codage en uint8 et en rune au sein de la même structure permet d’optimiser l’espace mémoire au détriment de la simplicité d’utilisation. Il fallait faire un choix et dans ce cas, c’est l’optimisation mémoire qui a gagné, à vous de faire attention aux données que vous manipulez...

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 27

Les VariabLes

GesTIon Des PoInTeUrs par Tristan Colombo

Qui dit manipulation de données en mémoire, dit adresse mémoire. Et qui dit adresse mémoire, dit pointeur, la bête noire de nombreux développeurs amateurs ou étudiants. Démystifions la croyance populaire qui veut que le pointeur soit le mal personnifié...

Une variable est une donnée à laquelle on peut ac-céder à n’importe quel moment. Pour cela, il faut donc qu’elle soit stockée en machine en utilisant un

composant capable de répondre rapidement : la mémoire vive. Mais comme la mémoire peut contenir de nombreuses données, il faut avoir des informations précises pour pouvoir récupérer une valeur : par où commencer la lecture et où s’arrêter ? Le début de la zone mémoire est donné par l’adresse et le type de la variable permet de savoir combien de blocs d’adresse devront être lus : pour un int32 il faudra lire 4 octets, pour un uint8 il faudra lire un seul octet, etc. La figure 1 montre le mécanisme de stockage d’une variable en mémoire.

Fig. 1 : Stockage d’une variable en mémoire

D’un point de vue syntaxique, la gestion des pointeurs en Go s’effectue de manière très similaire à celle du C... mais en plus simple !

1 Travailler avec les adresses mémoire

Pour toute variable, il est possible d’obtenir son adresse mémoire en faisant précéder son identifiant du caractère &.

Par exemple, pour une variable i, le contenu de la variable sera accessible par i et l’adresse de la variable sera don-née par &i :

var i int32 = 12fmt.Printf("Valeur de i : %d\n", i)fmt.Printf("Adresse de i : %p\n", &i)

Vous reconnaissez également l’écriture employée avec fmt.Scanf() pour lire des données saisies au clavier. Comme cette fonction modifie le contenu de la variable qui lui est passée en paramètre, ce n’est pas du contenu de la variable dont elle a besoin, mais de son adresse mémoire. C’est ainsi que pour enregistrer des données on utilise la syntaxe :

var i int32fmt.Scanf("%d", &i)

Une fois que la valeur est stockée en mémoire, on y accède simplement par i :

fmt.Printf("Valeur de i : %d\n", i)

Ceci est l’utilisation la plus simple des pointeurs : l’al-location mémoire est réalisée lors de la déclaration de la variable et nous n’utilisons l’adresse mémoire que dans des cas bien précis. Voyons maintenant comment vraiment créer une variable de type pointeur et réserver l’espace mémoire dont elle aura besoin.

2 Créer un pointeurLorsqu’une variable est déclarée en tant que pointeur,

l’identifiant de la variable ne représente plus le contenu de la variable mais l’adresse de celle-ci. Pour accéder au contenu de la variable, il faudra indiquer que l’on souhaite accéder à la valeur pointée par celle-ci. Cela se fait à l’aide du caractère * ajouté devant l’identifiant de la variable.

Pour débuter par un exemple simple, nous pouvons créer une référence à une variable existante : les deux variables de noms différents pointeront sur le même espace mémoire et le fait de modifier l’une modifiera l’autre :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com28

Les VariabLes GestioN des PoiNteurs

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var x int32 = 12 07: y := &x08: 09: fmt.Printf("Valeur de x : %d\n", x) 10: fmt.Printf("Adresse de x : %p\n", &x) 11: fmt.Printf("Valeur de y : %d\n", *y)12: fmt.Printf("Adresse de y : %p\n", y) 13: }

Nous commençons par définir ici une variable de type int32 en ligne 6. Il s’agit d’une déclaration classique, qui va donc réserver quatre octets en mémoire pour stocker un entier. x représente la valeur stockée en mémoire et &x est l’adresse mémoire permettant d’accéder à la valeur. En ligne 7, nous déclarons une deuxième variable, mais de manière dynamique cette fois-ci : y est un pointeur sur x puisque nous y stockons l’adresse mémoire de x. Le type de y est donc « pointeur sur variable de type int32 » et cela se note *int32. Donc, si nous voulons bien comprendre ce qui se passe, la ligne 7 pourrait s’écrire différemment à l’aide d’une déclaration statique : var y *int32 = &x. Les lignes suivantes permettent d’affi-cher les valeurs et adresses mémoire de x et de y. Notez bien les différences d’écriture :

- x est une variable « classique » : x représente la valeur et &x représente l’adresse mémoire ;

- y est un pointeur : *y représente la valeur et y repré-sente l’adresse mémoire.

Si nous exécutons ce code, nous obtiendrons l’affichage suivant (l’adresse mémoire variera bien sûr) :

Valeur de x : 12 Adresse de x : 0xf840038100 Valeur de y : 12 Adresse de y : 0xf840038100

Vous pouvez constater que x et y ont la même adresse mémoire. Donc, si en ligne 8 nous ajoutons une instruction du type *y = 5, nous modifierons la zone mémoire de y et de &x et donc, x aura également pour valeur 5. La figure 2 illustre cet exemple.

Fig. 2 : Création d’un pointeur sur une variable existante

Nous avons utilisé ici un pointeur sur un espace mémoire qui était déjà alloué grâce à la création d’une variable. Voyons maintenant comment allouer la mémoire nécessaire pour stocker des données. Il faut tout d’abord savoir qu’à sa création, un pointeur pointe forcément sur une adresse

mémoire... Quand il est créé sans allocation mémoire, une valeur particulière lui est assignée : nil. Cette valeur signifie « poin-teur non encore assigné » et la représentation de son adresse est 0x0. Cette déclaration pourra être faite de manière explicite ou implicite, les deux lignes suivantes étant équivalentes :

var x *int32var x *int32 = nil

En utilisant cette déclaration, aucun espace mémoire n’ayant été réservé, si vous voulez stocker une valeur dans x (*x = 12), vous obtiendrez une erreur à l’exécution (c’est toute la magie des pointeurs, on passe bien souvent l’étape de compilation sans difficulté) :

panic: runtime error: invalid memory address or nil pointer dereference

Il faut donc réserver l’espace mémoire correspondant au type de données que l’on souhaite stocker. En C, cette ligne est un peu complexe, alors qu’en Go, il suffit d’utiliser la fonction new() en indiquant en paramètre le type de données. Une fois cette commande utilisée, la variable fait référence à une zone mémoire contenant éventuellement des données : en C on peut accéder à ces données, en Go la zone mémoire est initialisée (0 pour un nombre, "" pour une chaîne de carac-tères, false pour un booléen, etc.). Voici comment réserver une zone mémoire pour stocker un entier :

var i *int32 = new(int32);

*i = 35fmt.Printf("Valeur de i : %t\n", *i)

On peut bien sûr réaliser cette opération en plusieurs étapes :

var i *int32 = nil

i = new(int32);*i = 35

Le pointeur sur void permettant de pointer vers n’importe quel type de variables n’existe pas en Go, il faudra utiliser une alternative du paquetage unsafe : unsafe.Pointer. Attention toutefois, ce paquetage ne porte pas son nom par hasard (unsafe donc dangereux).

01: package main 02: 03: import "fmt" 04: import "unsafe" 05: 06: func main() { 07: var ( 08: ptr unsafe.Pointer09: s string = "GLMF" 10: n int32 = 64 11: )12: 13: ptr = unsafe.Pointer(&n) 14: ptr = unsafe.Pointer(&s)15: fmt.Printf("Valeur de ptr : %s\n", *(*string)(ptr))16: fmt.Printf("Adresse de ptr : %p\n", ptr) 17: fmt.Printf("Valeur de s : %s\n", s) 18: fmt.Printf("Adresse de s : %p\n", &s) 19: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 29

Les VariabLes GestioN des PoiNteurs

www.gnulinuxmag.com

Dans cet exemple, nous déclarons plusieurs variables : une variable de type pointeur ptr, une chaîne de caractères s et un entier n (lignes 7 à 11). Le poin-teur fait simplement référence à une zone mémoire. Il peut donc pointer vers n’importe quel type de variable. Ainsi, en ligne 13, nous commençons par le faire pointer sur la variable entière, puis en ligne 14, nous le faisons pointer sur la chaîne de caractères. Lorsque nous affichons l’adresse mémoire de s (ligne 18) et la valeur de ptr (ligne 16), nous obtenons les mêmes résultats. Pour pouvoir afficher la valeur pointée par ptr, il va falloir effectuer une conversion, comme le montre la ligne 15. Cette conversion n’est pas des plus simples et peut se lire « valeur pointée par ptr considéré comme pointeur sur chaîne de caractères ».

Le langage Go dispose d’un ramasse-miettes (garbage collector en anglais) : pas de fonction delete() ou free() pour libérer la mémoire, le système s’oc-cupe de tout !

3 Utilisation avec les structuresLes pointeurs peuvent aussi s’utiliser avec les structures. Pour rappel, une

structure est un ensemble de champs. Pour définir une structure Person conte-nant un nom et un prénom, nous devrons écrire :

type Person struct { firstname, name string}

La création du pointeur se fera alors de manière tout à fait classique :

var linus *Person = new(Person)

Par contre, au niveau de l’utilisation, vous ne devrez pas préfixer l’identifiant de votre variable par * :

linus.firstname = "Linus"linus.name = "Torvalds"

fmt.Printf("Personne : %s %s\n", linus.firstname, linus.name)

Pour les structures, il existe un raccourci à new(Type) si vous souhaitez renseigner immédiatement les champs de votre structure : &Type{valeur_champ_1, ..., valeur_champ_n}. Sur l’exemple précédent, plutôt que de taper trois lignes pour créer et compléter notre variable, une seule aurait suffit :

var linus *Person = &Person{"Linus", "Torvalds"}

On crée en fait un élément de type Person et on affecte son adresse mémoire à linus grâce à &.

ConclusionPar rapport au C, les pointeurs ont été un peu dépoussiérés, mais il ne s’agit

pas d’un grand ménage de printemps... Certes, il y a de grandes simplifications au niveau de l’allocation mémoire et de la libération de celle-ci, puisque ce n’est plus nécessaire, mais la complexité générale des pointeurs est à peine voilée. Si vous n’aviez pas de problème pour les manipuler en C, vous n’aurez aucun problème en Go. Dans le cas contraire, il faudra forcément consacrer un peu de temps à la manipulation des pointeurs pour bien en comprendre le fonctionnement.

disponible chez votre marchand de journaux jusqu'au

30 novembre 2012 et sur : www.ed-diamond.com

GLMF N°154

Actuellement en kiosque !

DNS/Binddémystifié !

LE DÉpLOiEMENT

DE vOTrE SErvEUr

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com30

Les VariabLes tabLeaux, sLices et cartes

TabLeaUx, sLICes eT CarTespar Tristan Colombo

La variété et la simplicité (ou non) d’utilisation des structures dites complexes font qu’un langage sera plus ou moins apprécié des développeurs. Go propose trois structures... Il reste à voir si l’on peut les utiliser sans difficulté.

On distingue trois sortes de tableaux en Go : les tableaux, les slices (résultat d’une

opération de slicing ou découpage en tranches sur un tableau) et les cartes. Dans cet article, nous allons étudier ces différentes structures.

1 Les tableauxUn tableau est une suite de taille

fixe composée d’éléments de même type. Les tableaux peuvent posséder plusieurs dimensions, ce qui permettra, par exemple, de créer des matrices qui sont des tableaux à deux dimen-sions. Nous avons déjà eu un aperçu des tableaux en utilisant les strings, qui sont des tableaux de caractères. Tout ce que nous avions mention-né alors est applicable aux tableaux en général :

- les éléments sont indicés de 0 à n ;

- l’opérateur [] permet d’accéder directement à un élément dont l’indice est donné ;

- la fonction len() permet d’obtenir la taille du tableau. D’après cette donnée, le dernier élément a pour indice len() - 1 ;

Une propriété fondamentale est tou-tefois différente : les tableaux, contrai-rement aux strings, sont modifiables. Pour créer un tableau, trois syntaxes sont possibles :

- soit l’on souhaite travailler sur un tableau dont on connaît la taille mais pas encore le contenu et on utilise la syntaxe : [taille].Type ;

- soit les données du tableau sont connues et l’on désire initialiser le tableau à sa création : [taille].Type{valeur1, valeur2, ..., valeurn} ;

- soit les données sont connues, mais pas sa taille (qui peut être déduite du nombre de données) et nous laissons Go calculer la taille du tableau : [...].Type{valeur1, valeur2, ..., valeurn}.

Voici un exemple illustrant tout ce qui vient d’être expliqué :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var tab1 [5]int32 07: tab2 := [5]int32{1, 2, 3, 4, 5} 08: tab3 := [...]int32{6, 7, 8} 09: 10: tab1[0] = 10 11: tab2[2] = 0 12: 13: fmt.Printf("Taille de tab1 : %d\n", len(tab1)) 14: fmt.Printf("Element en 3eme position de tab2 : %d\n", tab2[2]) 15: fmt.Printf("Element 0 de tab3 : %d\n", tab3[0]) 16: }

Nous créons ici trois tableaux en utilisant chacune des syntaxes possibles. En ligne 6, il s’agit d’une création sans initialisation, alors que dans les lignes 7 et 8 nous initialisons les tableaux. Notez que pour ces deux dernières lignes, nous avons utilisé une déclaration dynamique, obligatoire avec cette syntaxe. En ligne 8, la taille du tableau est déterminée automatiquement par Go en fonc-tion des données ({6, 7, 8}, donc taille de trois éléments). En ligne 10, nous affectons une valeur en tant que premier élément du tableau tab1 et en ligne 11, nous montrons que les tableaux sont bien modifiables en remplaçant la valeur contenue à l’indice 2 dans tab2 (donc la valeur 3) par la valeur 0. Les lignes 13 à 15 permettent d’afficher dans l’ordre : la taille du tableau tab1 grâce à la fonction len() (même si nous n’avons stocké qu’un élément dans le tableau, la taille est celle fixée à la création, donc ici 5), le troisième élément de tab2 (donc l’élément ayant pour indice 2, puisque la numérotation commence à 0) et enfin, le premier élément de tab3.

Les éléments dont les valeurs ne sont pas explicitement données lors de la création des tableaux sont automatiquement initialisés à leur valeur par défaut (0 pour les entiers, "" pour les chaînes de caractères, etc.).

Les tableaux multidimensionnels utilisent la même syntaxe ; il suffit d’ajouter autant de tailles que de dimensions souhaitées. Par exemple, pour une matrice nous aurons :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 31

Les VariabLes tabLeaux, sLices et cartes

var matrix_1 [2][2]intmatrix_2:= [2][2]int{{0 ,0}, {0, 1}, {1, 0}, {1, 1}}

Pour parcourir les éléments d’un tableau à l’aide d’une boucle, nous pourrons utiliser l’instruction for associée à range. Nous avions laissé ce point en suspens dans l’article sur les structures de contrôle, il est donc temps d’y revenir. L’instruction range appliquée à un tableau renvoie les indices de ce tableau, ainsi que les éléments associés. Le deuxième paramètre peut être omis :

tab := [5]int32{1, 2, 3, 4, 5}

for index := range tab { fmt.Printf("Element %d : %d\n", index, tab[index])}

En utilisant le second paramètre, cette boucle peut être écrite :

for index, elt := range tab { fmt.Printf("Element %d : %d\n", index, elt)}

La première écriture est en fait un raccourci pour : for index, _ := range tab.

Lors du traitement en machine, les tableaux sont transmis par valeur : quand vous utiliserez une fonction, une copie entière du tableau sera effectuée. Cela signifie donc une perte de temps et une perte de mémoire ! Imaginez un tableau de 1 000 000 d’entiers : la place occupée en mémoire est de 4 000 000 oc-tets, soit un peu moins de 4 Mo. La place occupée est donc non négligeable... Et si vous transmettez ce tableau à une fonction, il y aura copie et donc, une occupation mémoire de 8 Mo ! Une solution pourra consister bien sûr à utiliser un pointeur pour éviter la copie, mais en Go, il existe une autre solution : les slices. En effet, ces structures sont transmises par référence.

2 Les slicesUn slice est une suite d’éléments de même type, comme les

tableaux. Par contre, bien que leur taille soit fixée au départ, elle peut évoluer soit en effectuant un slicing (diminution de la taille), soit en utilisant la fonction append() pour ajouter des éléments. Les slices peuvent être créés de plusieurs ma-nières : soit en utilisant un tableau classique sur lequel nous effectuerons une opération de slicing, soit en utilisant une déclaration classique de tableau, mais sans spécifier de taille, soit en utilisant la fonction make().

Pour vous prouver qu’un slice n’a pas le même type qu’un tableau, je vous propose d’utiliser le paquetage reflect et la fonction TypeOf() permettant de connaître le type d’une va-riable. Nous allons appliquer cette fonction sur un tableau tab et sur le slice tab[:] (équivalent de ce tableau puisqu’il s’agit d’une portion regroupant l’ensemble de ses éléments) :

01: package main 02: 03: import ( 04: "fmt" 05: "reflect" 06: ) 07: 08: func main() { 09: tab := [5]int32{1, 2, 3, 4, 5} 10: 11: fmt.Println(reflect.TypeOf(tab)) 12: fmt.Println(reflect.TypeOf(tab[:])) 13: }

En ligne 11, nous affichons le type de tab et en ligne 12 le type de tab[:]. Nous obtiendrons deux valeurs diffé-rentes : [5]int32 pour le premier et []int32 pour le second. Donc, si une variable prend pour valeur tab[:], elle aura un « type » slice.

Pour la création directe d’un slice, les syntaxes dispo-nibles sont les suivantes :

- make([]Type, taille) pour définir un slice pouvant contenir un nombre d’éléments connu ;

- make([]Type, taille, capacité) pour définir un slice d’une taille connue mais occupant éventuellement plus de place en mémoire (pour ajouter plus rapide-ment des éléments). En effet, à la création d’un slice, un tableau caché dont les éléments sont initialisés à leur valeur par défaut est créé, puis un pointeur vers ce tableau est utilisé. Ici, nous pouvons créer un tableau plus grand que la partie visible dans le slice. Le paramètre capacité sera toujours supérieur ou égal à la taille du slice. Pour connaître la capacité d’un slice, on pourra utiliser la fonction cap() ;

- []Type{} pour créer un slice vide. Équivalent de make([]Type, 0) ;

- []Type{valeur1, valeur2, ..., valeurn} pour définir un slice dont les valeurs sont connues.

L’utilisation des slices peut représenter un danger si l’on n’y prend pas garde. En effet, il ne faut pas oublier que l’on manipule des pointeurs de manière sous-jacente, comme l’illustre le code suivant :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: year := []string{"janvier", "février", "mars", "avril", "mai", "juin", 07: "juillet", "août", "septembre", "octobre", "novembre", "décembre"} 08: summer := year[5:9] 09: 10: fmt.Println(year) 11: fmt.Println(summer) 12: 13: summer[0] = "C’est l’été!" 14: 15: fmt.Println(year) 16: fmt.Println(summer) 17: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com32

Les VariabLes tabLeaux, sLices et cartes

Un premier slice est créé dans les lignes 6 et 7 : une liste des mois de l’année. En ligne 8, une nouvelle variable contenant les mois « juin » à « septembre » est créée. Lorsque nous affichons les données dans les lignes 10 et 11, nous obtenons bien les listes attendues :

[janvier février mars avril mai juin juillet août septembre octobre novembre décembre] [juin juillet août septembre]

Nous modifions ensuite le premier élément du slice summer (ligne 13). Mais comme nous travaillons sur des slices, modifier summer revient à modifier year... et le résultat nous le prouve :

[janvier février mars avril mai C’est l’été! juillet août septembre octobre novembre décembre] [C’est l’été! juillet août septembre]

Rappelez-vous donc toujours qu’un slice de tableau, au même titre qu’un slice de slice, est une référence et non une copie !

Pour parcourir tous les éléments d’un slice, comme avec les tableaux, vous pourrez utiliser la boucle for. En réutilisant le slice year de l’exemple précédent, cela donne :

for index, value := range(year) { fmt.Printf("Elt %d : %s\n",index, value)}

Les slices étant modifiables, il existe beaucoup plus d’outils disponibles pour ceux-ci que pour les tableaux. Je présente dans la suite quelques-unes des opérations possibles.

2.1 Modification d’un slice

Pour modifier un slice, il existe trois possibilités : supprimer des éléments, ajouter des éléments, ou remplacer les valeurs de certains éléments. Pour supprimer des éléments, il suffit de réaffecter un découpage sur le slice :

year = year[:5]

On ne conserve ici dans le slice year que les cinq premiers éléments. Pour

supprimer des éléments se trouvant ailleurs qu’en début ou fin de slice, il faudra concaténer des petites parties de slices en utilisant la technique d’ajout.

Pour ajouter des éléments, on utilise la fonction append(), qui prend en pa-ramètres le slice à agrandir et la liste des valeurs à ajouter. Cette fonction renvoie elle-même un slice qu’il faudra réaffecter au slice initial (si la taille du slice est suffisante, les éléments seront simplement ajoutés, sinon une nouvelle zone mémoire sera allouée, les anciennes valeurs y seront copiées et les nouvelles y seront ajoutées comme avec un realloc() en C) :

numbers := []int32{4, 5, 3, 2, 8, 1}numbers = append(numbers, 12, 54, 21)

Cette méthode ne permettra d’ajouter des éléments qu’à la fin du slice. Pour ajouter des éléments en tête ou au milieu du slice, il faudra procéder à la création d’un nouveau slice et à la copie de parties du slice original. Pour cela, nous utiliserons la fonction copy(), qui prend deux slices en paramètres, copie le contenu du second slice dans le premier et renvoie le nombre d’élé-ments copiés. Si nous voulons ajouter des éléments en tête de slice, voilà ce qu’il faudra écrire :

01: numbers := []int32{4, 5, 3, 2, 8, 1} 02: 03: new_slice := make([]int32, len(numbers) + 3) 04: n := copy(new_slice, []int32{0, 0, 0}) 05: copy(new_slice[n:], numbers)

Nous voulons ajouter trois entiers de valeur nulle en début du slice numbers défini en ligne 1. Pour cela, nous créons un nouveau slice vide qui contiendra des éléments du même type (ici int32) et qui aura la taille du slice original, augmentée du nombre d’éléments à ajouter (nous aurions pu écrire directement 9 dans cet exemple simple). En ligne 4, nous commençons par ajouter les premiers éléments du slice dans new_slice et nous récupérons dans n le nombre d’éléments ajoutés (forcément 3 ici). Pour finir, en ligne 5, nous ajoutons dans new_slice les éléments de numbers.

Notez que nous indiquons dans quelle partie du slice les données doivent être ajoutées par new_slice[n:] : à partir de l’élément d’indice n, donc après les éléments ajoutés en ligne 4.

Pour insérer des éléments au milieu d’un slice, la technique sera sensible-ment la même :

01: numbers := []int32{4, 5, 3, 2, 8, 1} 02: 03: new_slice := make([]int32, 9) 04: n := copy(new_slice, numbers[:2]) 05: m := copy(new_slice[n:], []int32{0, 0, 0}) 06: copy(new_slice[n+m:], numbers[2:])

On souha ite obten i r le sl ice {4, 5, 0, 0, 0, 3, 2, 8, 1}. Il faut donc dans un premier temps copier les éléments de numbers en position 0 et 1 à l’aide de numbers[:2] (ligne 4). Dans un deuxième temps, il faut insé-rer les nouvelles données (ligne 5). Et enfin, dans un troisième temps, il faut copier la dernière partie de numbers (donc numbers[2:]). La position de ces éléments dans le slice sera calculée en fonction du nombre d’éléments insérés dans les deux premières étapes avec n + m (ligne 6).

2.2 Le paquetage sort pour trier et rechercher dans un slice

Le paquetage sort fournit les outils pour trier et rechercher dans les slices contenant des entiers, des réels ou des chaînes de caractères. Ainsi, pour trier des tableaux dans l’ordre ascendant, nous pourrons respectivement utiliser Ints(), Float64s() et Strings(). Pour savoir si un tableau est trié, ce sera IntsAreSorted(), Float64sAre-Sorted(), ou StringsAreSorted(). Voici un exemple permettant de trier un slice d’entiers :

01: package main 02: 03: import ( 04: "fmt" 05: "sort" 06: ) 07: 08: func main() { 09: numbers := []int{4, 5, 3, 2, 8, 1} 10:

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 33

Les VariabLes tabLeaux, sLices et cartes

11: if !sort.IntsAreSorted(numbers) { 12: fmt.Println("Le tableau n’est pas trié!") 13: } 14: 15: sort.Ints(numbers) 16: fmt.Println("Tableau trié :") 17: fmt.Println(numbers) 18: }

Il faut nécessairement importer le paquetage sort (ligne 5). La seule chose à laquelle il faut faire attention est que Ints() ne travaille qu’avec des slices de type []int et non []int32 (int étant, je le rappelle, un alias de uint). Le reste du code est simple à comprendre et ne fait qu’utiliser les fonctions pré-citées.

Pour la recherche d’éléments, nous disposerons également de trois fonctions : SearchInts(), SearchFloat64s() et SearchStrings(). Ces fonctions pren-nent pour paramètres un slice trié et la valeur à rechercher. Elles renvoient la position de l’élément dans le slice et, si celui-ci ne s’y trouve pas ou si le slice n’est pas trié, elles renvoient la position du dernier élément non affecté (et non affectable) correspondant à la taille du slice :

sort.Ints(numbers)i := sort.SearchInts(numbers, 4)

3 Les cartesLes cartes, ou maps, sont des suites

de taille non fixée de paires clé/valeur. L’équivalent en Python se nomme « dic-tionnaire » et en Perl ou PHP il s’agit des « tableaux associatifs ». Les clés sont forcément uniques et d’un type sur lequel les opérateurs de comparaison fonctionnent. D’une part toutes les clés et d’autre part toutes les valeurs doivent posséder le même type. Il est impossible de faire un « panaché », comme avec les dictionnaires de Python. Il existe quatre syntaxes différentes permettant de créer des cartes :

- soit l’on veut créer une carte sans aucune valeur, on utilise map[TypeDesClés]TypeDesValeurs{}. La syntaxe make(map[TypeDesClés]TypeDes-Valeurs) est strictement identique ;

- soit l’on souhaite créer une carte et l’initialiser dans la foulée, il faudra alors écrire : map[TypeDesClés]TypeDesValeurs{clé1 : valeur1, ..., clén : valeurn}

- soit l’on désire créer une carte sans valeur, mais ayant déjà une taille déterminée (pour optimiser les ajouts sur une zone mémoire déjà réservée), la syntaxe sera make(map[TypeDesClés]Type-DesValeurs, capacité).

Une fois la carte créée, son utilisa-tion est très simple. L’ajout de données se fera comme pour une affectation dans un slice à l’aide de l’opérateur [] et en précisant la clé et la valeur : myMap[clé] = valeur. Lors de l’ac-cès à une donnée par [], ce n’est pas une information mais deux qui sont renvoyées : éventuellement la valeur correspondant à la clé et un booléen indiquant si la clé existe ou non dans la carte. On peut bien sûr préférer ne récupérer que la donnée et cela reste possible :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: linux := make(map[string]float32) 07: 08: linux["Debian"] = 6.0 09: linux["Ubuntu"] = 12.04 10: linux["Gentoo"] = 12.1 11: 12: version := linux["Debian"] 13: fmt.Printf("Dernière version de Debian %.2f\n", version); 14: }

En testant l’existence de la clé, on peut écrire de très jolies choses comme le code suivant, remplaçant le code des lignes 12 et 13 :

12: if version, found := linux["Fedora"]; !found {13: fmt.Println("Cette distribution n’est pas répertoriée.")14: } else {15: fmt.Printf("Dernière version de Debian %.2f\n", version);16: }

En ligne 12, on tente de récupérer la valeur associée à la clé « Fedora » dans la variable version et on récupère la valeur booléenne indiquant si la clé est

présente ou pas dans found. Tout ceci étant la phase d’initialisation de notre test conditionnel, nous testons ensuite la valeur de !found. Si l’élément n’est pas trouvé dans la carte, nous affichons le message de la ligne 13, sinon nous affichons le message de la ligne 14.

Pour modifier une valeur, il suffit d’allouer une nouvelle valeur à la clé qui y était attachée. Par exemple, si nous voulons modifier la valeur de la clé « Debian » définie en ligne 8, il faut écrire : linux["Debian"] = 6.1.1.

Pour supprimer une valeur, il faut supprimer la paire clé/valeur à l’aide de la fonction delete(), qui prend en paramètre la clé à supprimer. Le code delete(linux, "Gentoo") suppri-mera ainsi la paire Gentoo/12.1 de la carte linux.

Enfin, pour le parcours de cartes dans des boucles, ce sera encore la structure for...range qui sera employée. Dans ce contexte, la commande range renvoie deux données : la clé et la valeur qui lui est associée. Rappelez-vous toutefois que la carte n’est pas une structure ordonnée et que les éléments ne seront pas lus dans l’ordre de leur création :

for distrib, version := range linux { fmt.Printf("Distribution %s, version %.2f\n", distrib, version);}

ConclusionGo dispose de structures de données

intéressantes pour un langage compilé. Outre le traditionnel tableau, nous avons la possibilité de réaliser un découpage en tranches (slicing) et d’utiliser des structures de données efficaces, tant du point de vue de l’optimisation en termes de rapidité avec les slices, qu’en termes de confort de développement avec les maps. Les boucles sont adap-tées au parcours de ces structures et il en découle donc une grande souplesse d’utilisation. On retrouve vraiment de manière très claire la philosophie de Go avec ces structures : plus simple à développer et plus efficace.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com34

Pour aLLer PLus LoiN Les foNctioNs

Les fonCTIons par Tristan Colombo

Les fonctions permettent d’écrire un code réutilisable et paramétrable. Ça, tout le monde le sait. Mais chaque langage propose sa vision des fonctions, offrant de petites subtilités qui peuvent paraître anodines, mais qui le sont en fait rarement... Go ne déroge pas à la règle.

1 Déclarer une fonctionLa déclaration d’une fonction se fait à l’aide du mot-clé

func, suivi du nom de la fonction et éventuellement, de ses paramètres entre parenthèses. Vient ensuite le type de retour de la fonction, qui peut être une ou plusieurs valeur(s). D’un point de vue syntaxique, les différents cas de fonctions sans paramètres s’écriront de la manière suivante :

- fonction sans paramètre et ne renvoyant rien :

func nomDeLaFonction() { ...}

- fonction sans paramètre retournant une valeur :

func nomDeLaFonction() typeRetour { ...}

- fonction sans paramètre retournant plusieurs valeurs :

func nomDeLaFonction() (typeRetour_1, ..., typeRetour_n) { ...}

Pour les fonctions acceptant des paramètres, la syntaxe des trois types de retour sera identique à celle des fonc-tions sans paramètres, la seule différence provenant... de la déclaration des paramètres. Je ne détaillerai donc pas les trois cas et me contenterai de donner la syntaxe d’une fonction avec paramètre(s) retournant plusieurs valeurs :

func nomDeLaFonction(nomParamètre_1 typeParamètre_1, ..., nomParamètre_n typeParamètre_n) (typeRetour_1, ..., typeRetour_n) { ...}

Nous parlons depuis le début de « renvoyer des valeurs »... Mais comment faire ? L’instruction return remplit cette tâche. Attention, je dis bien instruction et non fonction, car comme dans la plupart des langages, return est une

instruction et s’écrit donc sans parenthèses (l’écriture sous forme de fonction ne génèrera toutefois pas d’erreur). return permet de renvoyer une ou plusieurs valeur(s) en les séparant par des virgules :

return valeur_1, ..., valeur_n

Prenons l’exemple d’un programme contenant une fonction permettant de faire la somme de quatre entiers et indiquant à l’aide d’un booléen si cette somme est paire ou non :

01: package main 02: 03: import "fmt" 04: 05: func sum(n1, n2, n3, n4 int32) (bool, int32) { 06: var s int32 = n1 + n2 + n3 + n4 07: return s % 2 == 0, s 08: }09: 10: func main() { 11: var n []int32 = []int32{1, 2, 3, 4} 12: pair, s := sum(n[0], n[1], n[2], n[3])13: if (pair) { 14: fmt.Println("Résultat pair") 15: } else { 16: fmt.Println("Résultat impair") 17: } 18: fmt.Printf("%d + %d + %d + %d = %d\n", n[0], n[1], n[2], n[3], s) 19: }

Dans les lignes 5 à 8, nous déclarons la fonction sum(), qui va renvoyer un booléen et un entier. Notez dans la signature de la fonction, en ligne 5, la factorisation du type des para-mètres : comme cette fonction accepte quatre paramètres de type entiers, inutile de répéter le type quatre fois. Sachez toutefois que la syntaxe suivante reste valable : func sum(n1 int32, n2 int32, n3 int32, n4 int32) (bool, int32). La factorisation de types est également disponible pour la déclaration de variables à l’aide du mot-clé var.

Après cette courte parenthèse, revenons au code. En ligne 6, nous créons une variable s qui contient le résultat de la somme des quatre entiers et en ligne 7, nous renvoyons deux valeurs : un booléen, qui teste si le reste de la division entière de s par deux est égal à 0 (cas d’un nombre pair), et la somme s.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 35

Pour aLLer PLus LoiN Les foNctioNs

Dans la fonction principale (lignes 10 à 19), nous commençons par créer un slice de quatre entiers en ligne 11. Pourquoi utiliser un slice ici ? Simplement pour mettre en pratique ce que nous avons vu dans les articles précédents... En ligne 12, nous appelons la fonction sum() et récupérons les deux valeurs qui nous sont transmises dans une variable pair (qui sera donc booléenne) et une va-riable s (qui sera entière). Ces valeurs sont ensuite utilisées pour afficher la parité de la somme (lignes 13 à 17) et la somme elle-même (ligne 18).

Notre exemple souffre d’un grave défaut : nous pouvons additionner quatre entiers... Mais si un jour nous avons besoin d’en additionner cinq, comment faire ? Il faudra écrire une seconde fonction... Et ce n’est pas très propre ! Il faudrait pouvoir préciser que le nombre de paramètres n’est pas fixé. Pour cela, il existe deux solutions : soit paramétrer la fonction par un slice, soit utiliser une syntaxe particulière indi-quant que les derniers paramètres sont de même type et que leur nombre n’est pas fixé. La deuxième solution est plus souple, plus « jolie » que la première. Nous allons quand même implémenter ces deux méthodes, ce qui nous permet-tra de voir un exemple d’utilisation de slice en tant que paramètre dans une fonction. Voici donc cette première solution (la numérotation des lignes est reprise depuis la ligne 5 du code précédent et correspond à la déclaration de la fonction) :

05: func sum(n []int32) (bool, int32) { 06: var s int32 = 0 07: for _, val := range n { 08: s += val 09: } 10: return s % 2 == 0, s 11: }

Le paramètre sous forme de slice est indiqué en ligne 8, par n []int32. La différence fondamentale par rapport à la version précédente se fera au niveau du traitement des données, puisqu’il va falloir itérer sur le slice. Cela est fait dans les lignes 7 à 9 pour calculer la valeur de la somme et la placer dans

la variable s. L’appel à cette fonction, qui se faisait en ligne 12 dans l’ancien code, sera bien entendu modifié et nous ne passerons plus qu’un seul paramètre slice : pair, s := sum(n). Cette écriture est concise et pratique, mais peu lisible et devient compliquée à utiliser si les données proviennent de sources différentes. C’est là que la seconde solution devient intéressante.

Pour indiquer un nombre variable de paramètres d’un certain type, il faudra faire précéder le nom du type des caractères .... Cette syntaxe ne pourra être utilisée qu’une seule fois dans la signature d’une fonction : dans le cas contraire, le compilateur serait incapable de déterminer de quel type devraient être les paramètres qui sont transmis lors de l’appel de la fonction. En fait, peu de choses vont être modifiées par rapport à la solution précédente. En effet, l’écriture du type de paramètre ...Type convertit les données en slice. Le traitement sera donc strictement identique :

05: func sum(n ...int32) (bool, int32) {06: var s int32 = 0 07: for _, val := range n { 08: s += val 09: } 10: return s % 2 == 0, s 11: }

L’appel de la fonction sera lui dif-férent, puisque nous devons passer en paramètres les différents entiers : pair, s := sum(n[0], n[1], n[2], n[3]).

Dans le cas des passages de para-mètres, nous pouvons être confrontés à un autre type de problème : les paramètres optionnels. Dans ce cas, des paramètres peuvent ne pas se voir affecter de valeur. En Go, ce problème est traité à l’aide des structures : il faut créer une structure où les différents champs seront initialisés à leur valeur par défaut. Si, lors du traitement, la valeur n’a pas été modifiée, c’est que le développeur n’a pas activé cette option. Nous allons ajouter deux options à notre exemple : displayCalc indiquera par un booléen s’il faut afficher toutes les

étapes du calcul et factor permettra de multiplier éventuellement le résultat final par un entier :

01: package main 02: 03: import "fmt" 04: 05: type Options struct { 06: displayCalc bool 07: factor int32 08: }09: 10: func sum(opt Options, n ...int32) (bool, int32) { 11: var s int32 = 0 12: for i, val := range n { 13: if opt.factor != 0 { 14: val *= opt.factor 15: }16: s += val 17: if opt.displayCalc { 18: fmt.Printf("Etape n.%2d : %2d\n", i + 1, s) 19: }20: } 21: return s % 2 == 0, s 22: } 23: 24: func main() { 25: var n []int32 = []int32{1, 2, 3, 4} 26: pair, s := sum(Options{}, n[0], n[1], n[2], n[3]) 27: if (pair) { 28: fmt.Println("Résultat pair") 29: } else { 30: fmt.Println("Résultat impair") 31: } 32: fmt.Printf("Somme : %d\n", s) 33: }

Dans les lignes 5 à 8, nous commençons par définir notre structure d’options avec ces deux champs. Lors de la déclaration de la fonction, en ligne 10, nous ajoutons le paramètre opt de type Options, qui permettra de gérer lesdites options. Les valeurs de ces champs sont ensuite testées dans le code de la fonction pour effectuer des tâches particulières : dans les lignes 13 à 15, si le champ factor a été modifié, nous multiplions la valeur courante par ce facteur (d’un point de vue d’optimisation du code, il serait plus intéressant de ne multiplier le résultat qu’à la fin en ligne 21, mais cela poserait problème avec l’option displayCalc qui n’afficherait plus le véritable calcul étape par étape).

Dans les lignes 17 à 19, si l’option displayCalc a été activée, nous af-fichons toutes les étapes du calcul.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com36

Pour aLLer PLus LoiN Les foNctioNs

Lors de l’appel de la fonction sum() en ligne 26, nous ajoutons la valeur du paramètre contenant les options par Options{}. Cette écriture nous permet de ne pas conserver en mémoire une variable qui ne servirait qu’une fois et nous indiquons que nous ne modifions pas les valeurs par défaut des options. Pour modifier les valeurs, il aurait fallu écrire par exemple : Options{displayCalc: true, factor: 5}. Notez que vous n’êtes pas obligé d’indiquer tous les champs de la structure, seuls ceux dont la valeur est modifiée sont nécessaires (si nous n’avions voulu qu’activer l’affichage à chaque étape nous aurions pu écrire Options{displayCalc: true}).

Dernier problème du passage de paramètres : le type générique. Pour ne pas fixer à l’avance le type des para-mètres, en Go il faudra utiliser des éléments particuliers : les interfaces.

2 Les interfaces en Go ne sont pas des interfaces...

En Go, les interfaces ne sont pas vraiment les interfaces telles qu’on les retrouve dans les langages orientés objet. Nous verrons d’ailleurs, dans l’article qui lui est consacré, que la programmation orientée objet en Go est très parti-culière. Pour revenir aux interfaces, une interface est un type qui spécifie un ensemble de signatures de méthodes. Tout type étant un sur-ensemble de l’interface peut contenir une variable du type de l’interface (nous approfondirons cette notion dans l’article sur la POO).

L’interface {} spécifiant un ensemble vide de méthodes, tout type vérifie cette interface. Nous pourrons donc l’utiliser pour rendre une fonction générique en indiquant un type de paramètre pouvant varier. D’un point de vue syntaxique, une interface est déclarée à l’aide du mot-clé interface et l’interface vide est interface{}. Pour déclarer une variable pouvant contenir n’importe quel type, c’est cette écriture que nous utiliserons :

var a interface{}

a = 5a = "Linux Magazine"

Nous n’obtiendrons pas de message d’erreur à la com-pilation : la variable a peut vraiment contenir n’importe quel type ! Utilisée avec des fonctions, cette technique nous permet de créer des fonctions pouvant prendre des paramètres de plusieurs types différents et renvoyant également un résultat pouvant être de différents types (il faudra alors convertir – ou « caster » – le résultat). Re-prenons l’exemple de la fonction sum() qui effectuait la somme de n entiers et transformons-la pour qu’elle puisse également traiter des réels et des chaînes de caractères (la somme sera alors une concaténation) :

01: package main 02: 03: import "fmt" 04: 05: func sum(n ...interface{}) (interface{}, bool) { 06: var result interface{} 07: 08: if len(n) == 0 { 09: return nil, false 10: } 11: 12: for index, value := range n { 13: if index == 0 { 14: result = value 15: } else { 16: switch val := value.(type) { 17: case string : result = result.(string) + val 18: case int : result = result.(int) + val 19: case float64 : result = result.(float64) + val 20: } 21: } 22: } 23: 24: return result, true 25: } 26: 27: func main() { 28: s, err := sum("Linux", " ", "Magazine") 29: s = s.(string) 30: 31: s2, err2 := sum(1.25, 3.14159, 65.43) 32: s2 = s2.(float64) 33: 34: if !err { 35: fmt.Println("Pas assez de paramètres pour sum() sur string!") 36: } else { 37: fmt.Println(s) 38: } 39: 40: if !err2 { 41: fmt.Println("Pas assez de paramètres pour sum() sur float32!") 42: } else { 43: fmt.Println(s2) 44: } 45: }

Dans les lignes 5 à 25, nous reprenons le code de la fonction sum() : elle admet maintenant un nombre non déterminé de paramètres de n’importe quel type (même si seulement les types string, int et float64 seront traités) et elle renvoie une donnée d’un type qui sera déterminé en interne d’après le type des paramètres et un booléen indiquant si le traitement a pu être achevé ou non (ligne 5).

En ligne 6, la variable qui contiendra le résultat que renverra la fonction est de type interface{}, puisque le type de retour est générique. Si aucun paramètre n’est transmis à la fonction (la longueur du slice n sera alors 0), nous renvoyons le couple nil, false pour indiquer une impossibilité de traiter des données... inexistantes. Sinon, nous parcourons la liste des paramètres et pour le premier d’entre eux nous initialisons la variable result (lignes 13 à 15). Pour les autres paramètres, en fonction de leur type (ligne 16), nous effectuons les calculs et conversions nécessaires (lignes 17 à 19).

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 37

Pour aLLer PLus LoiN Les foNctioNs

Nous utilisons ici une syntaxe du switch que nous n’avions pas abordée dans l’article traitant des boucles, ne connaissant pas encore le type interface{}. Dans le cadre d’un test de cas sur une variable de type interface{}, nous avons la possibilité de tester son type par l’intermédiaire d’une écriture semblable aux conversions : variable.(type). Il suffit alors de tester les différents types dans les structures case. Une astuce consiste à créer une variable dynamique sur la valeur de retour de variable.(type), car il s’agit de la variable convertie dans le type cité. Ceci évite des conversions multiples. Ainsi, sans cette astuce, les lignes 16 à 20 auraient dû être écrites :

16: switch value.(type) {17: case string : result = result.(string) + value.(string)18: case int : result = result.(int) + value.(string)19: case float64 : result = result.(float64) + value.(string)20: }

La définition de la fonction sum() effectuée, il ne reste plus qu’à l’utiliser en pensant à convertir la valeur de retour. Ainsi, la ligne 28 renvoyant dans s une chaîne de caractères, nous convertissons s en string en ligne 29 et le même pro-cédé se retrouve dans les lignes 31 et 32 avec des float64. Si la fonction n’avait renvoyé qu’un seul résultat de type interface{}, nous aurions pu récupérer et convertir le résultat sur une seule ligne du type : s := sum(...).(type).

Dans le cadre d’une utilisation lors de la définition de la signature de fonctions, les interfaces permettent donc d’in-troduire une dose supplémentaire de généricité.

3 La visibilitéUne variable définie dans le corps d’une fonction ne sera

visible qu’à l’intérieur de la fonction. À la fin de l’exécution de la fonction, celle-ci sera détruite. Seules les variables globales (déclarées à l’extérieur de toute fonction) seront accessibles depuis n’importe quelle fonction. Voici un petit code montrant la portée de plusieurs variables :

01: package main 02: 03: import "fmt" 04: 05: var s string = "Variable globale" 06: 07: func f1() { 08: var s string = "Variable locale à f1" 09: var b int32 = 10 10: fmt.Printf("Accès à s : %s\n", s) 11: fmt.Printf("Valeur de b : %d\n", b) 12: } 13: 14: func f2(a int32) { 15: a *= 2 16: fmt.Printf("Accès à s : %s\n", s) 17: } 18: 19: func main() { 20: var a int32 = 5 21:

22: f1() 23: // fmt.Printf("Valeur de b : %d\n", b) 24: 25: fmt.Printf("Valeur de a : %d\n", a) 26: f2(a) 27: fmt.Printf("Valeur de a : %d\n", a) 28: f2(a) 29: fmt.Printf("Valeur de a : %d\n", a) 30: 31: fmt.Printf("Accès à s : %s\n", s) 32: 33: fmt.Println("Visibilité de bloc :") 34: if a == 5 { 35: c := 12 36: fmt.Printf("Valeur de c : %d\n", c) 37: } 38: // fmt.Printf("Valeur de c : %d\n", c) 39: }

Plusieurs visibilités de variables sont testées dans ce code. Tout d’abord, nous déclarons une variable globale en ligne 5 (en toute rigueur, en suivant la convention de nommage de Go, nous aurions dû choisir un nom dont la première lettre était une majuscule). Cette variable est accessible depuis n’importe quel point de notre code : en ligne 16, dans le corps de la fonction f2() et en ligne 31, dans la fonction principale.

Le cas de la fonction f1() des lignes 7 à 12 est un peu particulier : nous avons bien sûr accès à la variable glo-bale s, mais comme nous définissons une variable locale de même nom, elle va masquer la première variable et en ligne 10 nous afficherons la valeur de la variable locale. Nous pouvons constater qu’à la fin de l’exécution de la fonction, la variable globale s n’est plus masquée (lignes 16 et 31). Dans la fonction f1(), nous définissons également une autre variable locale b (ligne 9) qui sera visible dans la fonction (ligne 11), mais qui n’existera plus à l’extérieur (si l’on dé-commente la ligne 23, le code ne compile plus).

La variable a définie en ligne 20 et initialisée avec la valeur 5 sera transmise à la fonction f2() (lignes 14 à 17). Bien que portant le même nom que le paramètre de f2(), nous travaillerons sur une copie de a puisque l’opération de la ligne 15 n’aura aucun impact sur la valeur de la va-riable a en sortie de la fonction : l’affichage des lignes 25, 27, et 29 sera le même. Si nous avions voulu pouvoir modifier la valeur de la variable a de la fonction main(), il aurait fallu utiliser des pointeurs :

func f2(a *int32) { *a *= 2 fmt.Printf("Accès à s : %s\n", s) }

L’appel à la fonction aurait alors été : f2(&a). Pour finir, les lignes 33 à 38 illustrent une portée de bloc : la variable c étant déclarée en ligne 35 dans le bloc du if, celle-ci n’est visible que dans ce bloc.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com38

Pour aLLer PLus LoiN Les foNctioNs

4 L’intérêt des fermetures

En Go, les fonctions anonymes sont appelées fermetures. Elles ont la spé-cificité de pouvoir utiliser les valeurs des variables et des constantes qui se trouvent dans la même zone de visibilité, comme le montre l’exemple suivant :

01: package main 02: 03: import "fmt" 04: 05: func main() { 06: var a int32 = 5 07: var fct func()08: 09: if a == 5 { 10: b := 10 11: fct = func() { fmt.Printf("Valeur de b : %d\n", b) }12: b = 513: } 14: 15: fct() 16: }

En ligne 7, nous définissons une variable fct de type func() (donc pointeur sur fonction). Cette variable servira à stocker une fonction anonyme de manière à tester l’influence de la zone de visibilité. On peut bien sûr utiliser les fonctions anonymes en tant que paramètres d’autres fonctions comme nous le verrons dans la suite. En ligne 9, un test est effectué sur la valeur de la variable a. Ce test ne sert qu’à créer un nouveau bloc dans lequel nous allons définir une nouvelle variable b (ligne 10), qui sera utilisée dans la définition de la fonction anonyme (ligne 11). Cette fonction ne fait qu’afficher la valeur de la variable b, valeur qui est changée en ligne 12.

En ligne 15, nous appelons la fonction anonyme par fct() (du coup la fonction n’est plus vraiment anonyme, mais cette technique permet de définir une fonction dans un sous-bloc, ce qui est normalement interdit). Le résultat de cet appel est l’affichage de la valeur 5. Le corps de la fonction n’était donc pas figé, mais faisait bien référence à l’adresse de la variable b.

5 Les fonctions en tant que paramètres

Il est possible de passer des fonc-tions en tant que paramètres d’autres fonctions. Pour cela, il faudra donner une signature de fonction en tant que paramètre et utiliser cette fonction dans le code. Lors de l’appel, il faudra donner le code de la fonction passée en paramètre sous la forme d’une fonction anonyme ou d’un pointeur sur fonction. Voici un exemple où une fonction va modifier un slice d’entiers en appliquant sur chacun de ses éléments une autre fonction passée en paramètre :

01: package main 02: 03: import "fmt" 04: 05: func mapSliceInt(s []int, fct func(i int) int) {06: for index, value := range s { 07: s[index] = fct(value) 08: } 09: } 10: 11: func main() { 12: var slice []int = []int{1, 2, 3, 4} 13: fmt.Println(slice) 14: mapSliceInt(slice, func(i int) int { return 2 * i})15: fmt.Println(slice) 16: }

La fonction mapSliceInt() des lignes 5 à 9 admet deux paramètres : un slice d’entiers et une fonction fct prenant en paramètre un entier et renvoyant un entier. Pour modifier le slice, il faut le parcourir à l’aide d’une boucle (ligne 6) en récupérant l’indice et la valeur de chaque élément. On peut ensuite modifier la valeur de cet élément dans le slice (ligne 7). Dans cette ligne, la variable value a en fait pour valeur s[index]. Nous aurions pu écrire les lignes suivantes qui auraient été équivalentes :

for index, _:= range s { s[index] = fct(s[index])}

mapSliceInt() est ensuite appelée dans la fonction principale. Nous avons besoin pour cela d’un slice d’entiers qui sera créé en ligne 12. Nous affichons

les valeurs du slice avant traitement (ligne 13), puis nous appelons la fonc-tion mapSliceInt() en lui passant en paramètres notre slice et une fonction anonyme renvoyant le double de l’entier qui lui a été passé en paramètre. En ligne 15, nous affichons enfin le slice après traitement.

Pour rappel, les slices sont toujours passés par adresse et donc, les modifica-tions effectuées sur le slice à l’intérieur de la fonction restent persistantes. Au final, nous obtenons bien l’affichage du slice original suivi du slice où toutes les valeurs ont été doublées :

[1 2 3 4] [2 4 6 8]

Si la fonction passée en paramètre est susceptible d’être réutilisée, il serait plus judicieux de la transmettre sous forme de pointeur plutôt que de fonction anonyme. Pour cela, il faut commencer par définir la fonction :

func double(i int) int { return 2 * i}

Il suffit ensuite de transmettre le nom de la fonction, puisque celui-ci contient l’adresse mémoire de la fonc-tion elle-même :

mapSliceInt(slice, double)

Ce mécanisme permet d’écrire des fonctions génériques de manière très simple.

6 optimisationDans cette partie d’optimisation

du code des fonctions, je voudrais commencer par un petit rappel sur la distinction entre code itératif et récursif. Une fonction récursive est une fonction qui fait appel à elle-même pour calculer un résultat. Si l’on prend les précautions d’usage (s’assurer qu’il existe des cas terminaux), cette écri-ture mathématique est très élégante, comme le montre l’exemple suivant appliqué au calcul d’une factorielle (n! = 1 x 2 x ... x n et 0! = 1) :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 39

Pour aLLer PLus LoiN Les foNctioNs

01: func fact(n int32) int32 { 02: if n == 0 || n == 1 { 03: return 1 04: } 05: return n * fact(n - 1) 06: }

Dans cette fonction, si le paramètre n vaut 0 ou 1, le résultat renvoyé est 1 et sinon, il est calculé par n x fact(n-1). Lors de l’exécution de l’instruction return, nous quittons la fonction. Tout ce qui se trouve après le return ne sera donc jamais exécuté. Donc, si le test de la ligne 2 est vérifié, la ligne 3 sera exécutée et nous quitterons la fonction. Ce mécanisme permet de ne pas écrire :

if n == 0 || n == 1 { return 1} else { return n * fact(n – 1)}

Cette écriture ne compile pas en Go, car pour le compilateur il est possible de sortir de la fonction sans passer par une instruction return. Ceci est bien sûr faux, mais nous oblige à faire l’économie d’une instruction else.

Cette version de la factorielle nous donne le bon résultat et son écriture est très proche d’une écriture mathématique. Voyons maintenant la version itérative :

01: func fact(n int32) int32 { 02: var result int32 = 1 03: var i int32 04: for i = 1; i <= n; i++ { 05: result *= i 06: } 07: return result 08: }

Nous aurons besoin ici d’une variable intermédiaire result, qui sera initialisée à 1 (élément neutre pour la multiplica-tion). Le résultat final sera calculé par itérations successives (lignes 4 à 6). Notez que si vous voulez spécifier préci-sément le type d’entiers (ici int32), vous ne pourrez pas utiliser la déclaration dynamique for i := 1 ; i <= n ; i++ qui crée un entier de type int et ne pourra pas comparer un int et un int32 (types différents).

Cette version de la factorielle donne le même résultat que la version précédente,

mais elle est un peu plus difficile à lire... Et pourtant, c’est cette dernière qui est la plus efficace ! En effet, les fonctions récursives doivent stocker en mémoire des calculs intermédiaires et parcourir deux fois leur arbre de calcul. Elles sont donc plus lentes et plus gourmandes en mémoire, comme le montre la figure 1.

Fig. 1 : Comparaison du calcul de 4! en utilisant la fonction fact() récursive et en utilisant sa version itérative.

Une fonction est dite « pure » lorsqu’elle ne possède pas d’effets de bord et que, pour un ou des argument(s) donné(s), elle renvoie toujours le même résultat. C’est le cas de toutes les fonctions mathématiques qui ne font pas appel à de l’aléatoire (en tout cas, il faut l’espérer...). On peut optimiser le traitement de ces fonctions particulières si l’on sait que l’on va les appeler plusieurs fois avec les mêmes arguments.

Grâce à la technique dite de la « mémoization », chaque appel à une fonction pure va donner lieu à un test : si cette fonction n’a jamais été appelée avec les mêmes arguments, le résultat est calculé, puis stocké en mémoire ; sinon, si la fonction a déjà été appelée avec les mêmes arguments, le résultat est directe-ment récupéré en mémoire et renvoyé. Ce mécanisme n’est pas particulier à Go et s’implémente en créant une « sur-fonction » qui conservera des résultats dans une variable de cache. Voici une implémentation de la mémoization sur la fonction de calcul de la factorielle récursive (la mémoization est aussi applicable aux fonctions itératives, mais le gain de temps sera forcément moins important) :

01: package main 02: 03: import ( 04: "fmt" 05: "time" 06: ) 07: 08: func fact(n int32) int32 { 09: if n == 0 || n == 1 { 10: return 1 11: } 12: return n * fact(n - 1) 13: } 14: 15: func memoizeFact(n int32, cache map[int32]int32) int32 { 16: if value, found := cache[n]; found { 17: return value 18: } 19: cache[n] = fact(n) 20: return cache[n] 21: } 22: 23: func main() { 24: cache := make(map[int32]int32) 25: 26: for i := 1; i <=2; i++ { 27: start := time.Now().Nanosecond()

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com40

Pour aLLer PLus LoiN Les foNctioNs

28: m := memoizeFact(12345, cache) 29: end := time.Now().Nanosecond() 30: fmt.Printf("Call %d\n", i) 31: fmt.Printf("Elapsed time : %f\n", float32(end - start)/float32(time.Second)) 32: fmt.Printf("Result : %d\n", m) 33: fmt.Println("----------------------------\n") 34: } 35: }

Pour pouvoir évaluer le temps gagné, nous avons im-porté un paquetage supplémentaire : time (ligne 5). Dans les lignes 8 à 13, nous retrouvons la fonction récursive fact() telle que nous l’avions écrite. Les lignes 15 à 21 contiennent le code de la fonction d’encapsulation de fact(). Cette fonction, nommée memoizeFact(), prend les mêmes paramètres que fact() auxquels il faut ajou-ter une carte qui nous servira de cache. memoizeFact() renvoie le même type de données que fact(), puisqu’elle ne fait qu’intercepter une demande de calcul adressée à fact().

En ligne 16, nous testons si le cache contient le résultat du calcul de fact(n) (la variable cache[n] contiendra le résultat de fact(n)). Si le calcul a déjà été effectué et sau-vegardé, la variable found vaudra true et la ligne 17 sera exécutée et renverra le résultat. Sinon, nous calculerons le résultat et nous le placerons dans le cache (ligne 19) avant de renvoyer le résultat (ligne 20).

Il ne reste plus qu’à lancer deux calculs pour tester le temps mis pour obtenir le résultat. Dans la fonction principale (lignes 23 à 35), nous créons une carte qui servira de cache (ligne 24), puis nous lançons une boucle pour effectuer deux calculs (lignes 26 à 34). Dans cette boucle, nous commençons par stocker l’heure de départ en nanosecondes (ligne 27), puis nous calculons une factorielle grâce à la fonction memoizeFact() (ligne 28), et nous stockons l’heure de fin du calcul (ligne 29). Ceci nous permet d’afficher le temps passé pour cal-culer le résultat (ligne 31). La conversion en secondes est effectuée grâce à la constante time.Second du paquetage time.

Pour finir, nous affichons le résultat du calcul (ligne 32). La différence de durée est flagrante, comme le montre ce résultat d’exécution :

Call 1 Elapsed time : 0.000415 Result : 0 ---------------------------- Call 2 Elapsed time : 0.000001 Result : 0

Si vous devez utiliser un même cache pour plusieurs fonctions, il sera préférable de le déclarer en tant que variable globale.

7 fonctions particulièresNous avons déjà vu et largement utilisé une fonction par-

ticulière de Go : la fonction main(). Cette fonction ne peut se trouver que dans le paquetage principal appelé main et sera exécutée au lancement du code compilé.

Il existe une seconde fonction spéciale, la fonction init(). Cette fonction est complètement optionnelle et pourra se situer dans n’importe quel paquetage (voir article suivant pour plus de détails sur les paquetages). Elle sera exécutée avant tout autre appel (mais les constantes et les variables globales du paquetage seront lues avant). Voici un petit exemple illustrant l’utilisation de cette fonction :

01: package main 02: 03: import "fmt" 04: 05: const N int32 = 1 06: 07: var msg string = "Linux Pratique" 08: 09: func init() { 10: fmt.Println("Initialisation") 11: fmt.Printf("Accès à la constante N : %d\n", N) 12: fmt.Printf("Accès à la variable msg : %s\n", msg) 13: msg = "Linux Magazine" 14: } 15: 16: func main() { 17: fmt.Println("\nMain") 18: fmt.Printf("Accès à la constante N : %d\n", N) 19: fmt.Printf("Accès à la variable msg : %s\n", msg) 20: }

Nous voyons bien que la fonction init() peut utiliser constantes et variables globales (lignes 11 à 12) et même modifier les variables (ligne 13). Comme ce code est exécuté avant la fonction main(), les affichages de cette dernière tein-dront compte des modifications effectuées dans init(). Ainsi, l’appel à la ligne 19 affiche « Accès à la variable msg : Linux Magazine » et non « Accès à la variable msg : Linux Pratique » puisque la variable a été modifiée en ligne 13.

ConclusionLa définition de fonctions en Go est à la fois classique et

originale. Classique par sa structure et originale par ses diffé-rentes possibilités : retour de plusieurs valeurs typées, nombre de paramètres non fixé et typage générique. Go introduit ainsi une souplesse sans équivalent dans les langages typés dont il s’inspire. Le développeur utilisant des langages typés y verra une simplification de son travail et le développeur utilisant des langages à typage dynamique ne sera pas accablé par une soudaine complexité. L’alliance des deux mondes produit un résultat très intéressant...

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 41

Pour aLLer PLus LoiN

Les PaqUeTaGes par Tristan Colombo

Inutile d’encombrer la mémoire des machines avec du code qui ne sera jamais utilisé. Pour « étendre » Go, on utilise les paquetages qui vont permettre d’effectuer des actions de plus haut niveau, sans avoir à réinventer la roue (éventuellement carrée...). Si le domaine sur lequel on souhaite intervenir n’a pas encore été traité par un paquetage, nous aurons la possibilité de créer le nôtre.

Un paquetage est un fichier de code Go proposant prin-cipalement des fonctions qui

seront normalement regroupées par thème (si un paquetage vous propose à la fois des outils permettant de générer du code HTML et de communiquer en Bluetooth, passez votre chemin : le développeur est atteint d’un syndrome de dédoublement de personnalité). Le paquetage est donc une bibliothèque de code (souvent appelée librairie à cause d’une mauvaise traduction du terme anglais library). Si vous déve-loppez en Python, le paquetage Go est l’équivalent du module. Tout fichier de code Go est un paquetage. Pour s’en convaincre, il suffit de lire la première ligne qui commencera toujours par package nom.

Un paquetage peut proposer des variables, des constantes, des types personnalisés ou des fonctions. Ces différents objets pourront être réutilisés dans un autre paquetage, en réalisant un import grâce à l ’instruction de même nom. Une fois importés, les éléments sont ac-cessibles en préfixant leur nom par le nom du paquetage. Ainsi, si l’on réalise l’import du paquetage math, la fonction Cos() qu’il définit sera accessible par math.Cos().

Go propose de très nombreux paquetages qui sont référencés sur la page http://www.golang.org/pkg

(d’autres paquetages sont disséminés sur la page http://godashboard.appspot.com). Vous trouverez pêle-mêle des paquetages de traitement des images, de cryptographie, de compression, etc.

La gestion des paquetages a été pensée depuis leur création jusqu’à leur installation et leur utilisation pour être le plus simple possible. Dans cet article, nous allons voir comment créer un paquetage en respectant les conventions (avec notamment l’écriture de la documentation et des tests), puis nous étudierons la commande go dans le cadre de la gestion des paquetages.

1 Création d’un paquetageNous avons vu qu’un paquetage était un simple fichier de code Go.

Pour pouvoir importer un paquetage, il faut que celui-ci se trouve dans un répertoire accessible depuis la variable d’environnement GOPATH ou GOROOT pour un accès global. Par convention, un paquetage de nom myPackage.go doit se trouver dans un répertoire portant le nom myPackage. Si vous suivez l’arborescence standard, vous devriez à minima avoir un ré-pertoire src contenant le répertoire de votre projet (considéré comme pa-quetage), contenant lui-même le fichier principal et un répertoire incluant le paquetage :

src/ └── monProjet ├── monPaquetage │ └── monPaquetage.go └── monProjet.go (contient la fonction main())

Voici un exemple de création et d’utilisation d’un paquetage fournissant deux fonctions de calcul. Nous considérerons que notre projet s’appelle paquetages et nous aurons alors l’arborescence suivante :

src └── paquetages ├── mycalc │ └── mycalc.go └── paquetages.go

Le programme principal se trouvera dans le fichier paquetages.go et le paquetage proprement dit dans mycalc.go. Voici son code :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com42

Pour aLLer PLus LoiN Les PaquetaGes

01: package mycalc 02: 03: func Sum(n0 float64, n ...float64) float64 { 04: result := n0 05: 06: for _, value := range n { 07: result += value 08: } 09: 10: return result 11: } 12: 13: func Mymap(f func (float64) float64, n []float64) []float64 { 14: result := n 15: 16: for index, value := range n { 17: result[index] = f(value) 18: } 19: 20: return result 21: }

Dans ce fichier, on peut noter en première ligne le nom du paquetage donné par l’instruction package (mycalc). Deux fonctions sont définies : la fonction Sum() dans les lignes 3 à 11 et la fonction Mymap() dans les lignes 13 à 21. La fonction Sum() calcule et renvoie la somme des réels qui lui sont passés en paramètres.

Une astuce est utilisée pour s’assurer qu’au moins un réel sera passé en paramètre : dans la signature de la fonction, nous signalons dans la liste des paramètres un réel suivi d’un slice de réels. Ainsi, inutile d’ajouter un test sur la taille du slice : si la fonction est utilisée sans paramètre, elle produira une erreur à la compilation.

La fonction Mymap() permet d’appliquer une fonction passée en paramètre à l’ensemble des valeurs contenues dans le slice passé lui aussi en paramètre. La fonction devra admettre un paramètre de type réel et renvoyer un résultat réel (voir ligne 13).

Attention : vous aurez peut-être remarqué qu’ici le nom des fonctions du paquetage commençait par une majuscule... Il doit commencer obligatoirement par une majuscule pour que les fonctions puissent être utilisées lors d’un import. Avec une minuscule en première lettre, vous obtiendrez le message d’erreur suivant :

# paquetages src/paquetages/paquetages.go:9: cannot refer to unexported name mycalc.sum

Pour utiliser ce paquetage, nous écrirons le code de paquetages.go :

01: package main 02: 03: import ( 04: "fmt" 05: "paquetages/mycalc" 06: )

07: 08: func double(n float64) float64 { 09: return 2 * n 10: } 11: 12: func main() { 13: var s []float64 = []float64{25, 12.2, 33} 14: 15: fmt.Println(mycalc.Sum(1.25, 34.4, 50.2)) 16: 17: fmt.Println(s) 18: mycalc.Mymap(double, s) 19: fmt.Println(s) 20: }

L’import de notre paquetage se fait en ligne 5, où le nom du paquetage (mycalc) est préfixé par le nom du projet (paquetages). On retrouve donc exactement l’arborescence utilisée pour stocker les fichiers. Si vous souhaitez créer des sous-paquetages, il vous suffit de créer des sous-répertoires pour y placer vos fichiers. Pour utiliser la fonction Mymap(), nous définissons une fonction double() dans les lignes 8 à 10.

L’appel des fonctions de notre paquetage se fait ensuite simplement en préfixant leur nom par le nom du paquetage (lignes 15 et 18). Si la fonction double() n’est utilisée que pour appeler Mymap(), il est possible de l’écrire sous forme de fermeture :

mycalc.Mymap(func (n float64) float64 { return 2 * n }, s)

Vous aurez remarqué qu’il n’y a bien sûr qu’une fonction main(). Par contre, nous aurions pu écrire une fonction init() à la fois dans le fichier paquetages.go et dans le fichier mycalc.go. Le schéma de la figure 1 illustre l’ordre de char-gement et d’exécution de plusieurs paquetages.

Fig. 1 : Ordre de chargement et d’exécution de plusieurs paquetages

Pour compiler le code, il faudra utiliser la traditionnelle commande go avec go build ou go install. En utilisant go install (et en ayant créé l’arborescence standard), vous pourrez voir qu’en plus du fichier exécutable placé dans le répertoire bin, un nouveau fichier est apparu dans le réper-toire pkg : pkg/linux_amd64/paquetages/mycalc.a (le nom de répertoire suivant pkg dépend de la plateforme de

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 43

Pour aLLer PLus LoiN Les PaquetaGes

compilation). Les fichiers d’extension .a sont des fichiers de bibliothèque sta-tique : tant que vous ne modifiez pas votre paquetage, inutile de re-compiler son code.

2 La documentation

Lorsque l’on travaille à plusieurs développeurs sur un même projet ou que l’on propose des bibliothèques de code qui seront utilisées dans différents projets, la documentation technique est essentielle. Même si cela est parfois très intéressant, on ne va pas constamment ouvrir les fichiers de code pour étudier les fonctions et comprendre comment elles doivent être appelées et qu’est-ce qu’elles sont censées faire !

Si vous avez enregistré vos paquetages dans un répertoire accessible depuis GOPATH ou GOROOT, la documentation sera mise à jour automatiquement et en utilisant la commande godoc, vous pourrez afficher la documentation relative à votre paquetage.

En utilisant notre exemple précédent, la commande godoc paquetages nous indiquera l’existence d’un sous-paquetage mycalc. Pour un paquetage installé dans un répertoire « fantaisiste »,

vous pourrez utiliser le paramètre -path pour indiquer le chemin de recherche. En utilisant le paramètre -http, vous lancerez le serveur de documentation. Ainsi, en exécutant godoc -http=:8000, la documentation sera disponible pour consultation depuis un navigateur à l’adresse http://localhost:8000. En cliquant sur le lien « Packages » en haut à droite, puis en recherchant le lien « paquetages -> mycalc », on obtient la documentation du paquetage visible en figure 2. La page est très claire et bien organisée, mais ne contient aucune explication ! C’est normal : nous n’avons rien écrit ! Mais vous pouvez constater, que même sans travail de notre part, un squelette de documentation existe.

Fig. 2 : Documentation du paquetage mycalc sans avoir rien écrit...

Pour écrire la documentation, il faudra faire précéder les lignes à documenter par des commentaires (lignes débutant par //). Vous n’aurez pas besoin d’ap-prendre une syntaxe particulière pour rédiger votre documentation (en tout cas pour l’instant). Il faut juste savoir trois choses :

- Pour la documentation d’un paquetage (avant l’instruction package), la première ligne est un résumé de la description et sera affichée en vis-à-vis du nom du paquetage dans la liste des paquetages, et après une ligne vide se trouve la description complète du paquetage, que l’on retrouvera sur la page qui lui est dédiée ;

- Pour aller à la ligne, il faut indiquer une ligne de commentaire vide ;- Le fait d’utiliser au moins une indentation dans les commentaires indiquera

qu’il s’agit de code (représentation différente sur la page HTML).

Si nous souhaitons documenter notre paquetage mycalc, voici ce que nous pourrions écrire (une partie de la documentation résultante est présentée en figure 3) :

01: // Paquetage de calcul pour GLMF HS Go 02: // 03: // Ce paquetage permet d’illustrer l’article sur les paquetages en Go. Il 04: // contient 2 fonctions de calcul qui seront appelées depuis le fichier 05: // paquetages.go.06: package mycalc 07: 08: // Fonction permettant de réaliser la somme de réels (au moins un réel doit 09: // être passé en paramètre). Sum() retourne un réel.10: //

Fig. 3

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com44

Pour aLLer PLus LoiN Les PaquetaGes

11: // Exemple d’utilisation : 12: // s := Sum(12, 3.14, 55.6)13: func Sum(n0 float64, n ...float64) float64 { 14: result := n0 15: 16: for _, value := range n { 17: result += value 18: } 19: 20: return result 21: } 22:23: // Fonction appliquant une fonction appliquée en paramètre à l’ensemble des 24: // valeurs contenues dans le slice de réels qui lui est également passé en 25: // paramètre. 26: // 27: // Exemple d’utilisation : 28: // var s []float64 = []float64{25, 12.2, 33}29: // mycalc.Mymap(func (n float64) float64 { return 2 * n }, s)30: func Mymap(f func (float64) float64, n []float64) []float64 { 31: result := n 32: 33: for index, value := range n { 34: result[index] = f(value) 35: } 36: 37: return result 38: }

3 Les testsLes tests unitaires permettent de s’assurer du bon fonctionnement d’un code

et de la non régression de celui-ci à l’ajout d’une nouvelle fonctionnalité ou à la correction d’un bug. Dans le cadre des paquetages qui peuvent être distribués pour que d’autres développeurs puissent profiter de votre travail, il est bien évi-demment très important d’implémenter ces tests. Un article entier est consacré à leur écriture dans la suite de ce hors-série.

4 La commande go et les paquetagesLa commande go admet plusieurs options liées à la gestion des paquetages.

4.1 Lister et afficher des informations sur les paquetages

go list permet d’afficher les paquetages disponibles dans le répertoire cou-rant. L’option -json permet d’obtenir de nombreux renseignements (notamment les dépendances) sur le paquetage au format JSON. En exécutant la commande go list -json dans le répertoire contenant paquetages.go, nous obtenons :

{ "Dir": ".../src/paquetages", "ImportPath": "paquetages", "Name": "main", "Target": ".../bin/paquetages", "Stale": true, "Root": "...", "GoFiles": [ "paquetages.go" ], "Imports": [ "fmt", "paquetages/mycalc" ],

"Deps": [ "errors", "fmt", "io", "math", "os", "paquetages/mycalc", ..., "unsafe" ] }

De nombreuses informations sont disponibles. Si nous avions documenté notre fichier, nous aurions pu obtenir la ligne de résumé décrivant le paquetage, mais nous avons surtout accès à la liste des dépendances des paquetages. Bien sûr, nous n’avons pas explicitement importé tous ces paquetages, mais les paquetages réalisant chacun des imports, la liste s’allonge rapidement.

4.2 Tester un paquetageLa commande go test permet de

tester des paquetages. Cette commande sera vue dans l’article portant sur les tests unitaires.

4.3 obtenir et installer un paquetage

La commande go get télécharge un paquetage et l’installe. La gestion des dépendances se fait de manière automatique : si des paquetages requis sont absents du système, ils seront téléchargés et installés. Si le paquetage est déjà présent sur la machine, il faut exécuter go install pour le compiler et l’installer. Par exemple, pour télécharger et installer goplay, il faut exécuter : go get github.com/kless/goplay.

ConclusionLa simplicité de création des modules

Python a été réemployée ici pour la création des paquetages. Ajouté à cela, on trouve la gestion des dépendances et de l’instal-lation par la commande go et un système de documentation technique entièrement automatisé. Bien utilisés, tous ces outils permettent de produire rapidement du code de qualité. Évidemment, si vous n’écrivez ni la documentation, ni les tests, ce n’est pas Go qui va les écrire pour vous...

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Référence Prix / No Qté Total

ExEmplE : LM111 6,50 € 1 6,50 €

ToTal :FRAiS De PoRT FRANCe MÉTRo. : +3,9 €

FRAiS De PoRT HoRS FRANCe MÉTRo. : +6 €

ToTal :

Complétez votre ColleCtion

D'anCiens numéros

les 4 façons De CommanDer !par courrier : en nous renvoyant ce bon de commande.

par le Web : sur notre site : www.ed-diamond.com.

par téléphone : entre 9h-12h & 14h-18h au 03 67 10 00 20 (paiement C.B.)

par fax : au 03 67 10 00 21 (C.B. et/ou bon de commande administratif)

Je choisis de régler par : Chèque bancaire ou postal à l’ordre des éditions Diamond

 Carte bancaire n°

Expire le :

Cryptogramme visuel :

Date et signature obligatoire

Voici mes coordonnées postales :

Nom :

Prénom :

Adresse :

Code Postal :

Ville :

Téléphone :

e-mail :

  Je souhaite recevoir des infos des Edtions Diamond Je souhaite recevoir des infos des partenaires des Editions Diamond

Bon de commandeà remplir (ou photocopier) et à retourner aux Éditions Diamond - GNU/linux Magazine - BP 20142 - 67603 Sélestat Cedex / France

N° 133

du numéro... ...au numéro

N° 100 N° 153

Bon de commande GNU/linux Magazine Hors-sérieRéf. Désignation Prix / Nos

LMHS52 Développement ANDRoiD 6,50 €LMHS53 initiation à PyTHON 6,50 €LMHS54 Spécial PHP - introduction, programmation objet et optimisation du code 6,50 €LMHS55 Spécial C & C++ 6,50 €LMHS56 Java - Rendez vos développements multiplateformes ! 6,50 €LMHS57 Carnet de RooT 6,50 €LMHS58 ZeND Framework 2 6,50 €LMHS59 Développer des applications web plus rapidement et avec moins de code ? Django 8,00 €LMHS60 20 Recettes pour développer vos applications Android 8,00 €LMHS61 Créez vos applications Android comme un pro ! 8,00 €LMHS62 Ne quittez plus vos serveurs des yeux ! La supervision avec Shinken 8,00 €

du numéro... ...au numéro

HS N° 52 HS N° 62HS N° 61

Bon de commande GNU/linux MagazineRéf. Désignation Prix / Nos

LM100 Le serveur pARFAIT + BoNUS : plus de 550 Articles de GLMF sur CD 7,50 €

LM133 Clustering et systèmes de fichiers répartis 6,50 €LM134 Virtualisation avec XeN 4 6,50 €LM136 Besoin d'un annuaire pour enfin tout centraliser ? oPeNLDAP - installation - Sécurisation - Réplication 6,50 €LM137 Renforcez la sécurité de vos connexions distantes avec oPeNSSH et les certificats 6,50 €LM138 Centralisez la gestion des authentifications X.509 + SSH 6,50 €LM139 Découvrez les nouveautés DeBiAN 6.0 6,50 €LM140 Simplifiez et automatisez la gestion de votre virtualisation 6,50 €LM141 Noyau & CGRoUPS 6,50 €LM142 Testez le CLoUD 6,50 €LM143 installez une solution Single Sign-on complète et multiplateforme avec Kerberos 6,50 €LM144 Jouons avec le Kernel ! 6,50 €LM145 Gérez vos sources & projets proprement ! - avec Git et Redmine 6,50 €LM146 Débarrassez-vous de votre serveur mail ! Grâce à Google 6,50 €LM148 Visite au cœur de l'émulateur QeMU 7,50 €LM149 Migrez votre système de fichiers vers BTRFS ! 7,50 €LM150 Créez votre VPN avec oPeNVPN 7,50 €LM151 installez votre groupware Kolab 7,50 €LM152 Protégez vos applications web 7,50 €LM153 Systemd prêt à remplacer init ... ou pas 7,50 €

Retrouvez les sommaires et commandez tous nos magazines sur notre site :

http://www.ed-diamond.com

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Complétez votre collection de Hors-Série

Choisissez vos numéros dans le tableau ci-dessous : N°8 Spécial Sécurité

N°9 Installer son serveur web à la maison

N°10 Complétez l'installation de votre serveur internet

N°14 Maîtrisez Blender

N°18 Haute disponibilité

N°20 PHP 5 - Tout pour débuter et progresser avec PHP

N°24 Linux embarqué

N°28 Spécial Administration DEBIAN

N°29 BSD - Acte 1

N°30 BSD - Acte 2

N°32 Virus - Unix, Gnu/Linux et Mac OS X

N°33 Ruby & Ruby On Rails

N°34 Dominez votre système et allez au-delà de l'interface graphique

N°35 Serveur Web - installez configurez et optimisez votre serveur HTTP !

N°36 Serveur Mail - Installez configurez et optimisez votre serveur SMTP !

N°37 Serveur Dédié - Supervision, Streaming, VoIP, VPN, Syslog

N°38 Électronique, embarqué & domotique - 100% pratique

N°39 Cartes à puce - Administration et utilisation

N°40 Explorez les richesses du langage PYTHON

N°41 Configurez et optimisez votre FIREWALL

N°42 Supervision & Surveillance - Gardez un œil sur votre réseau et vos utilisateurs

N°43 Électronique, embarqué et hacks

N°44 Introduction, configuration et utilisation avancée de PostgreSQL 8.4

N°45 Retours d'expériences pour sysadmins 10 solutions concrètes

N°46 Focus sur les outils pour mieux exploiter Linux

N°47 Voyage au centre de l'Embarqué...

N°48 Besoin d'un serveur polyvalent, rapide et sur mesure ?

N°49 Code, Applicatifs, Projets, ... Incontournable Python !

N°50 Installation, configuration et optimisation de votre serveur web apache

N°51 Hacks, électronique & embarqué

Bon de commande à remplir (ou photocopier) et à retourner aux Éditions Diamond - GNU/linux Magazine Hors-Série - BP 20142 - 67603 Sélestat Cedex

Numéros GNU/Linux Magazine épuisés : N°1 à N°7, N°11 à N°13, N°15 à N°17, N°19, N°21 à N°23, N°25 à N°27, N°31

Je choisis de régler par : Chèque bancaire ou postal à l’ordre des éditions Diamond

 Carte bancaire n°

Expire le :

Cryptogramme visuel :

Date et signature obligatoire

Voici mes coordonnées postales :

Nom :

Prénom :

Adresse :

Code Postal :

Ville :

Téléphone :

e-mail :

  Je souhaite recevoir des infos des édtions Diamond Je souhaite recevoir des infos des partenaires des éditions Diamond

Quantité Prix / No Total

x 3,25 € =

FRAiS De PoRT FRANCe MÉTRo. : + 3,90 €FRAiS De PoRT HoRS FRANCe MÉTRo. : NOUS CONSULTER

ToTal :

* dans la limite des stocks disponibles.

au tarif promotionnel de 3,25 €ttC par numéro* !Rendez-vous sur www.ed-diamond.com pour consulter le sommaire détaillé de chaque magazine !

Vous recherchez un numéro spécifique ?

Les 4 façons de commander !Par courrier en nous renvoyant ce bon de commande.

Par le WebSur notre site : www.ed-diamond.com.

Par téléphoneentre 9h-12h & 14h-18h au 03 67 10 00 20 (paiement C.B.)

Par faxAu 03 67 10 00 21 (C.B. et/ou bon de commande administratif)

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 47

Pour aLLer PLus LoiN

La ProGraMMaTIon orIenTÉe objeT en Go

par Tristan Colombo

La majorité des langages modernes sont des langages orientés objet. Nous n’avons jusqu’à présent utilisé que de la programmation impérative en Go. Peut-on développer en utilisant une architecture orientée objet ? Si un article entier est présent dans ce hors-série, vous devez vous douter de la réponse. Mais peut-être serez-vous surpris par la mise en œuvre et la conclusion...

En programmation orientée objet (encore appelée POO), les mêmes concepts sont utilisés

de la phase d’analyse jusqu’à la concep-tion. Cette méthode de développement est issue du génie logiciel et permet de suivre le logiciel tout au long du cycle de sa vie (expression des besoins, analyse, conception, tests, etc.). La POO a été créée pour répondre à la gestion de gros projets impliquant de nombreux développeurs. À ce titre, il est très important de respec-ter des règles de codage : nomenclature homogène, indentation correcte du code, documentation de code, etc. La documen-tation technique du code est également essentielle : chaque fonction doit être commentée de manière pertinente pour permettre à un futur développeur de reprendre facilement le code.

En Go, ces points sont en partie réglés par la commande gofmt permettant de formater le code et par la commande godoc. Il faudra toutefois garder à l’es-prit que ces outils ne font pas tout. Un code harmonieux ne pourra être produit que si tous les développeurs travaillent réellement ensemble en faisant des com-promis. Prenez l’exemple d’un livre rédigé par plusieurs auteurs. Pensez-vous que si chaque paragraphe était écrit par un auteur différent, conservant son style très caractéristique, le livre serait agréable à lire ? Non, bien sûr... Il en va de même pour les codes !

Dans cet article, nous commencerons par un rapide rappel des concepts fon-damentaux de la programmation orientée objet, de manière à pouvoir étudier point par point comment ils sont traités en Go.

1 Pour se rafraîchir la mémoire1.1 Classes, attributs et méthodes

Une classe est une structure de données qui unit la liste des attributs d’un objet avec la liste des méthodes qui opèrent dessus. Plutôt que d’avoir des variables éparses qui modélisent un objet (une voiture par exemple), et d’avoir des fonc-tions qui peuvent s’appliquer sur ces variables, la POO permet de tout regrouper au sein d’un objet : la classe. Il est important pour la suite de se rappeler qu’il est tout à fait possible de simuler ce fonctionnement avec un langage impératif.

Voyons un exemple d’implémentation d’une classe Voiture en Java (POO) et en C (impératif). Cette classe représente une voiture et possède trois attributs (marque, modele et prix) qui peuvent être manipulés à l’aide de trois méthodes (creation, augmente_prix, diminue_prix) :

Java C

class Voiture{ int prix; String marque; String modele;

void creation(String m, String model, int p) { marque = m; modele = model; prix = p; }

void augmente_prix(int p) { prix = prix + p; }

void diminue_prix(int p) { prix = prix – p; }}

typedef struct{ int prix; char *marque; char *modele;} Voiture;

void creation(Voiture *v, char *m, char *model, int p){ v->marque = m; v->modele = model; v->prix = p;}

void augmente_prix(Voiture *v, int p){ v->prix = v->prix + p;}

void diminue_prix(Voiture *v, int p){ v->prix = v->prix – p;}

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com48

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

Grâce à cette définition, nous pourrons ensuite déclarer, en fonction du langage, des objets ou des variables de type Voiture :

Java C

Voiture cabriolet;cabriolet = new Voiture();

Voiture* cabriolet;cabriolet = (Voiture*) malloc(sizeof(Voiture));

La variable cabriolet est appelée instance de la classe Voiture. Depuis cet élément, nous pouvons accéder à ses attributs ou déclencher l’exécution de méthodes.

1.1.1 attributsChaque attribut ou méthode peut avoir une visibilité

différente : publique, privée ou protégée. Ces visibilités ne sont pas toutes implémentées dans tous les langages. Prenons un exemple simple pour décrire les différentes visibilités : un clavier. Si vous allez dans un cybercafé, le clavier sera utilisé par de nombreuses personnes : il est public. Le clavier de votre ordinateur personnel est, quant à lui, privé si vous êtes le seul à l’utiliser. Si vous décidez de prêter votre clavier uniquement à certains membres de votre entourage, on pourra dire que vous le protégez des autres utilisateurs (Fig. 1).

La visibilité des différents éléments permet la mise en place d’une notion fondamentale de la programmation orientée objet : l’encapsulation. Elle consiste à masquer le plus possible les détails d’implémentation et le fonction-nement interne des objets. Cette dissimulation permet de masquer la complexité de la classe et permet également de préserver l’intégrité des attributs. En effet, si la valeur d’un attribut peut être modifiée depuis n’importe quel endroit, il n’y a plus aucun contrôle sur sa valeur ! Si on interdit l’accès direct et que la modification passe obliga-toirement par une méthode, on assure un contrôle sur la valeur qui sera stockée.

Fig. 1 : Modes d’accès à un objet

1.1.2 MéthodesLes méthodes sont des fonctions attachées à un objet

et qui peuvent travailler directement avec ses attributs. Ces méthodes peuvent être surchargées, c’est-à-dire que l’on peut définir plusieurs fois une même méthode avec

une même signature (même nom et même nombre et type de paramètres), à condition de les définir dans des classes diffé-rentes, ou encore avec des signatures différentes au sein d’une même classe (cela est également valable pour le constructeur). Lors de l’appel de la méthode, en fonction des paramètres qui seront passés, le branchement vers la méthode correspondante s’effectuera automatiquement. Voici un exemple en Java sur la classe Voiture :

void augmente_prix(int prix){ this.prix = this.prix + prix;}

void augmente_prix(int prix, int rabais){ this.prix = this.prix + prix - rabais;}

void augmente_prix(double pourcentage){ this.prix = this.prix + this.prix*pourcentage;}

Lorsqu’un objet n’est plus référencé, que plus aucune variable ne contient son adresse, un mécanisme s’occupe de libérer automatiquement la mémoire. Ce mécanisme se nomme « ramasse-miettes » (ou garbage collector en anglais). Comme l’appel au ramasse-miettes est automatique, on ne sait pas à quel moment précis il sera appelé. On peut toutefois utiliser une méthode particulière, qui s’exécutera juste avant la destruction de l’objet (par exemple, en Java, il s’agit de la méthode finalize()).

1.1.3 attributs et méthodes de classeCertains attributs peuvent être « partagés » par tous les

objets d’une même classe : il existe alors un seul exemplaire de cet attribut qui peut être modifié par n’importe quelle instance de l’objet. Un attribut ayant cette propriété est un attribut statique ou attribut de classe.

Les méthodes statiques ou méthodes de classes s’exécu-tent en référence à leur classe et non plus en référence à une instance particulière (il ne faut pas créer d’objet pour les exécuter). Ces méthodes ne peuvent désigner et modifier que des attributs de classe.

1.2 CompositionOn peut utiliser les objets comme des attributs. On parle

alors de composition de classes : une ou des classes sont utilisées pour créer une autre classe. Ce mécanisme per-met de ré-exploiter des objets existants pour en créer de nouveaux. Par exemple, si nous possédons un objet Roue, nous pourrons l’utiliser pour créer un objet Voiture (quatre roues), un objet Moto (deux roues) ou encore un objet Camion (douze roues).

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 49

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

1.3 HéritageEn POO, on essaye de segmenter le

code en petites unités réutilisables. Par « réutilisables », on entend bien sûr de ne pas avoir à réécrire tout le code. Par exemple, on souhaite pouvoir réutiliser un objet en y apportant quelques modifi-cations de comportement (au niveau des attributs et/ou des méthodes). On crée ainsi une nouvelle classe à partir d’une classe qui existe déjà, en la complétant pour qu’elle permette de créer des objets plus spécifiques (on part ainsi du plus générique et on « spécialise » l’objet de départ au fur et à mesure).

En restant sur l’exemple précédent, si nous considérons que les motos, les voitures et les camions ont tous des roues, mais en nombre différent, on peut dire que ce sont tous des véhicules, mais avec chacun des caractéristiques particulières. La figure 2 représente le diagramme de classes UML de cet exemple.

Fig. 2 : Diagramme représentant les notions d’héritage entre différents

véhicules. Notez la présence de l’objet « Roue » qui est utilisé en tant qu’attribut de l’objet « Vehicule » (composition) et qui est donc un attribut des différents

véhicules spécialisés (« Moto », etc.). Ce diagramme a été réalisé avec le logiciel de

modélisation UML Umbrello.

1.4 PolymorphismeLe polymorphisme consiste à changer

le comportement d’une classe grâce à l’héritage, tout en continuant à l’utiliser comme une classe de base.

Que peut-on faire avec le poly-morphisme ? On peut affecter à une variable a de type A (une instance de

la classe A) la valeur d’une variable b de type B, classe fille de A. a référence alors le même objet que référence b. On dit alors que le type de a est devenu B de façon dynamique. Il est alors pos-sible d’appeler des méthodes définies dans B sur a :

- si la méthode appelée n’est définie que dans la classe mère, elle est invoquée ;

- si la méthode appelée a été redéfi-nie dans la classe fille, c’est cette dernière qui est invoquée ;

- si la méthode appelée n’est définie que dans la classe fille, l’appel direct provoque une erreur de type à la compilation : il faut transtyper a en B pour appeler la méthode de la classe fille.

Voici un exemple simple de polymor-phisme en Java :

01: class A 02: { 03: void hello() 04: { 05: System.out.println("Salut"); 06: } 07: } 08: 09: class B extends A 10: { 11: void ciao() 12: { 13: System.out.println("Au revoir"); 14: } 15: } 16: 17: class Test 18: { 19: public static void main(String[] argv) 20: { 21: A a = new A(); 22: B b = new B(); 23: 24: a.hello(); 25: //a.ciao(); // Erreur : a ne dispose pas de la méthode ciao() 26: a = b; // Mais attention : b = a provoque une erreur ! 27: //a.ciao(); // Erreur de type 28: ((B) a).ciao();29: } 30: }

1.5 Classes abstraites et interfaces1.5.1 Classes abstraites

Une classe abstraite sert à modéliser des types d’objets qui n’ont pas de réalité concrète. Pour illustrer cette définition,

prenons l’exemple d’un moyen de trans-port : il s’agit d’une notion abstraite signifiant simplement que cet « objet » va nous permettre de nous rendre d’un point A à un point B. Par contre, un vélo, une voiture ou un bus sont des objets concrets et ce sont tous des moyens de transport. On peut donc factoriser tous les comportements communs aux objets « vélo », « voiture » et « bus » dans l’objet « moyen de transport ». Par contre, les actions spécifiques à chacun ne seront pas définies dans l’objet « moyen de transport ». Une classe abstraite per-met ainsi de modéliser des objets dont les comportements sont voisins et qui possèdent un code commun.

Du point de vue de l’implémentation, une classe abstraite contient en géné-ral un certain nombre de méthodes « concrètes » et des méthodes abstraites, qui sont des déclarations de méthodes contenant leur signature mais pas leur code (ces méthodes seront définies dans les classes filles). Une classe est dite abstraite dès qu’elle contient au moins une méthode abstraite.

1.5.2 InterfacesNous avons déjà utilisé des interfaces

en Go donc attention, il s’agit ici des interfaces dans le sens qui est donné en programmation orientée objet. Une interface sert à indiquer les méthodes qu’une classe met à la disposition des autres classes, mais sans donner son implémentation ni sa structure interne. Ce système permet de fournir une meilleure modularité du code et une plus grande robustesse. Ainsi, lorsqu’une application nécessite de définir de nombreuses classes qui vont interagir et qui seront écrites par des développeurs différents, chaque programmeur n’aura besoin de connaître que les interfaces des classes qu’il n’a pas à écrire, mais dont les classes qu’il écrit ont besoin pour fonctionner.

1.6 Traitement des erreurs par exceptions

Une exception est un cas particulier d’information non standard que doit traiter une méthode et qui ne peut pas

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com50

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

être traitée par la méthode, ou néces-site un traitement très spécifique. Les raisons de ce traitement peuvent être diverses : erreur de programmation, erreur d’entrée/sortie, valeur non au-torisée passée en paramètre, etc.

Pour récupérer et traiter une ex-ception, il faudra des structures de type try {...} catch (...) {...}, où dans le bloc try on effectue les traitements souhaités qui peuvent renvoyer éventuellement une exception et dans le (ou les) bloc(s) catch, un peu comme dans un switch, on récupère les différentes exceptions et on indique leur traitement.

2 Ce que l’on fait en Go

Voici maintenant comment la program-mation orientée objet est traitée en Go. Je préfère vous prévenir : attendez-vous à quelque chose de radicalement diffé-rent de ce qui est fait classiquement !

2.1 Classes, attributs et méthodes

En Go, on ne parlera plus d’objets, de classes et d’instances, mais de types et de valeurs. Définir un objet Point, représentant un point en coordon-nées cartésiennes, revient à créer un type Point :

type Point struct { x, y int}

Donc, si nous continuons à utiliser le langage classique de la programmation orientée objet, pour créer une instance de Point il suffit de créer une variable de ce type :

var p Point

2.1.1 attributsL’accès aux attributs se fait à l’aide

de l’opérateur « point » :

01: package main 02: 03: import "fmt"

04: 05: type Point struct { 06: x, y int 07: } 08: 09: func main() { 10: var p Point 11: p.x = 5 12: p.y = 1 13: fmt.Printf("p: (%d, %d)", p.x, p.y) 14: }

Il n’y a que deux types de visibilité en Go : la visibilité publique, visible de partout, ou la visibilité paquetage (seulement à l’intérieur du paquetage de définition). Tous les identifiants commençant par une lettre majuscule sont exportés à l’extérieur du paquetage (visibilité publique) et forcément, ceux débutant par une lettre minuscule ont une visibilité paquetage. La mécanique de la convention de codage est reprise du langage Python. Vous obtenez donc ici une réponse à une question que vous vous êtes peut être posé en lisant l’article sur les paquetages : pourquoi utiliser obligatoirement un nom de fonction commençant par une majuscule ?

Nous illustrerons l’usage de la visibi-lité en Go une fois que nous aurons vu comment écrire des méthodes.

2.1.2 MéthodesLes méthodes en Go sont des fonc-

tions que l’on va « brider » pour un appel sur un type particulier. Au lieu d’écrire la signature de la fonction de manière classique, on préfixera le nom de la fonction par le nom du type sur lequel la fonction pourra être appelée. Concrètement, voici la syntaxe d’une fonction nommée maFonction() et d’une méthode nommée maMethode() pouvant être appliquée sur le type Point :

func maFonction(int a, string s) (int r, error err) { ...}

func (p Point) maMethode(int a) error err { ...}

En fonction du traitement effectué dans la méthode, le type défini avant le nom de la fonction pourra être un pointeur (s’il y a modification de la

valeur de l’un des champs) ou non. Reprenons l’exemple du Point auquel nous allons ajouter une « méthode » de translation et une autre indiquant s’il s’agit de l’origine ou non :

01: package main 02: 03: import "fmt" 04: 05: type Point struct { 06: x, y int 07: } 08: 09: func (p *Point) translate(dx, dy int) { 10: p.x += dx11: p.y += dy 12: } 13: 14: func (p Point) isOrigine() bool { 15: return p.x == 0 && p.y ==0 16: } 17: 18: func (p Point) toString() string { 19: return fmt.Sprintf("p: (%d, %d)", p.x, p.y) 20: } 21: 22: func main() { 23: var p Point 24: p.x = 5 25: p.y = 1 26: fmt.Println(p.toString()) 27: p.translate(-5, -1) 28: fmt.Println("Après translation de (-5, -1) :") 29: fmt.Println(p.toString()) 30: if p.isOrigine() { 31: fmt.Println("p se trouve à l’origine des axes") 32: } 33: }

Le type Point est défini dans les lignes 5 à 7. Trois méthodes sont as-sociées à ce type :

- translate() dans les lignes 9 à 12 : cette méthode modifie la valeur des champs x et y du Point sur lequel elle est appliquée. On voit donc que dans la signature de cette méthode, c’est le type pointeur sur Point (*Point) qui est utilisé ;

- isOrigine() dans les lignes 14 à 16 : cette méthode renvoie un booléen indiquant si les champs x et y sont tous les deux égaux à zéro. Comme il n’y a pas modification de valeur d’un champ du Point, la signature de cette méthode indique un type Point « standard » ;

- toString() dans les lignes 18 à 20 : il s’agit d’un raccourci pour afficher correctement la valeur des champs du type Point. Le nom de

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 51

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

cette méthode a été choisi par habitude de programmation, mais n’implique aucune propriété particulière. Il ne s’agit pas du toString() de Java ou du _ _str_ _() de Python.

Nous utilisons ensuite ces méthodes dans la fonction prin-cipale en utilisant l’opérateur « point » : appel de toString() dans les lignes 26 et 29, de translate() en ligne 27 et de isOrigine() en ligne 30.

Revenons sur l’utilisation de la visibilité. Si nous voulons encapsuler les champs x et y de Point, il va nous falloir créer deux paquetages : un paquetage contenant le type Point (la « classe ») et un paquetage de test. Commençons donc par le fichier Point.go (se trouvant dans un répertoire Point) :

01: package Point 02: 03: import "fmt" 04: 05: type Point struct { 06: x, y int 07: } 08: 09: func (p Point) GetX() int { 10: return p.x 11: } 12: 13: func (p *Point) SetX(value int) { 14: p.x = value 15: } 16: 17: func (p Point) GetY() int { 18: return p.y 19: } 20: 21: func (p *Point) SetY(value int) { 22: p.y = value 23: } 24: 25: 26: func New(x_val, y_val int) *Point { 27: return &Point{x : x_val, y : y_val} 28: } 29: 30: func (p *Point) Translate(dx, dy int) { 31: p.SetX(p.GetX() + dx) 32: p.SetY(p.GetY() + dy) 33: } 34: 35: func (p Point) IsOrigine() bool { 36: return p.GetX() == 0 && p.GetY() ==0 37: } 38: 39: func (p Point) ToString() string { 40: return fmt.Sprintf("p: (%d, %d)", p.GetX(), p.GetY()) 41: }

Le paquetage se nomme « Point » du nom de la « classe » ou plutôt du type que l’on définit (ligne 1). On retrouve la définition du type Point dans les lignes 5 à 7. Notez que si les noms des champs avaient comporté une majuscule en première lettre (ici simplement X et Y), ils auraient eu une visibilité publique et auraient donc été accessibles (en accès et en modification) depuis l’extérieur du paquetage. Inversement, si le nom du type n’avait pas comporté de majuscule (point au lieu de Point), nous n’aurions pas pu l’utiliser à l’extérieur du paquetage.

Nous avons ajouté quatre méthodes d’accès et de modi-fication en suivant le schéma d’encapsulation classique : les accesseurs GetX() (lignes 9 à 11) et GetY() (lignes 17 à 19), ainsi que les modifieurs SetX() (lignes 13 à 15) et SetY() (lignes 21 à 23). Comme nous n’aurons plus un accès direct aux champs pour les initialiser, nous créons une fonction New() dans les lignes 26 à 28. Cette fonc-tion va faire office de constructeur et va donc renvoyer un pointeur sur Point. La ligne 27 construit un Point en spécifiant les valeurs des champs x et y et renvoie son adresse grâce à &. Les fonctions suivantes n’ont été que légèrement modifiées de manière à implémenter l’encapsulation : pas d’accès ou de modification directe des champs du type Point.

Le fichier décrivant le type Point étant achevé, nous pouvons l’utiliser dans un programme en le chargeant sous forme de paquetage (fichier poo.go du projet poo, donc dans src/poo) :

01: package main 02: 03: import ( 04: "fmt" 05: "poo/Point" 06: ) 07: 08: func main() { 09: p := Point.New(5, 1) 10: //p.x = 5 11: //p.y = 1 12: fmt.Println(p.ToString()) 13: p.Translate(-5, -1) 14: fmt.Println("Après translation de (-5, -1) :") 15: fmt.Println(p.ToString()) 16: if p.IsOrigine() { 17: fmt.Println("p se trouve à l’origine des axes") 18: } 19: }

La création d’un Point se fait maintenant très simplement par appel à la fonction New(), comme le montre la ligne 9. Les lignes 10 et 11, commentées, ne peuvent plus fonc-tionner puisque le type Point se trouvant dans un autre paquetage, les champs ne sont plus visibles à l’extérieur de celui-ci. Ces lignes ont de toute façon été remplacées par l’appel à New(). Le code suivant est identique à ce que nous avions utilisé précédemment, si ce n’est la majuscule qui est apparue sur la première lettre de toutes les méthodes (sinon elles n’auraient pas été publiques et seraient donc restées inaccessibles).

Pour finir avec les généralités sur les méthodes, abordons la surcharge. Elle est impossible au sens où nous l’entendons dans les langages traditionnels. On peut créer des fonctions de même nom, mais elles ne porteront pas sur le même type d’objet. Par exemple, dans le paquetage Point, nous avons le droit d’écrire une seconde méthode toString(), mais celle-ci ne doit pas porter sur le type Point :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com52

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

func toString() { fmt.Println("Test de surcharge")}

2.1.3 attributs et méthodes de classeCes éléments n’existent pas en Go. En effet, il n’y en a pas

besoin : une variable globale avec une visibilité paquetage pourra compenser un attribut de classe et une fonction sans restriction de type remplacera une méthode de classe. Nous pourrions par exemple ajouter les lignes suivantes dans le fichier Point.go :

var pseudoStatic int = 0

func SetPseudoStatic(value int) { pseudoStatic = value}

func GetPseudoStatic() int { return pseudoStatic}

Le nom de la variable pseudoStatic commence bien par une lettre minuscule de manière à obtenir une visibilité paquetage. Les deux fonctions suivantes, SetPseudoSta-tic() et GetPseudoStatic() ont une visibilité publique et ont accès à la variable pseudoStatic puisque définies dans le même paquetage. Notez bien l’absence de définition d’un type d’application au niveau de la signature de ces deux méthodes. Pour utiliser cette variable et ces méthodes dans le paquetage main, nous nous retrouverons exactement dans la même situation qu’avec l’usage d’un attribut et de méthodes de classe :

01: package main 02: 03: import ( 04: "fmt" 05: "poo/Point" 06: ) 07: 08: func main() { 09: p := Point.New(5, 1) 10: p2 := Point.New(2, 3) 11: fmt.Printf("p => %s\n", p.ToString()) 12: fmt.Printf("p2 => %s\n", p2.ToString()) 13: fmt.Printf("PseudoStatic %d\n", Point.GetPseudoStatic()) 14: Point.SetPseudoStatic(25) 15: fmt.Printf("PseudoStatic %d\n", Point.GetPseudoStatic()) 16: }

Même en définissant deux variables distinctes de type Point dans les lignes 9 et 10, l’affichage de la valeur de la variable pseudoStatic ne sera pas modifié par p ou p2. Les appels aux « méthodes de classe » se font normalement en les préfixant par le nom du paquetage dans les lignes 13 à 15. Il n’y a donc aucun risque de les confondre avec des fonctions qui ne sont pas rattachées à notre type Point.

2.2 CompositionEn Go, la composition est très simple à mettre en place : il

suffit d’inclure dans la définition d’un nouveau type un ou des types définis précédemment. Par exemple, si nous voulons définir un triangle, nous pourrons réutiliser le type Point :

type Triangle struct { p1, p2, p3 Point}

Chaque champ de Triangle aura ainsi deux champs x et y de type entier. Ce type aurait également pu être écrit en utilisant un tableau :

type Triangle struct { p [3]Point}

La composition peut se faire également par enfouissement de type : il s’agit de définir un nouveau type dont l’un des champs sera d’un type précédemment défini. Si ce champ est anonyme – qu’aucun identifiant ne le désigne, les champs issus du champ enfoui seront accessibles exactement comme s’il s’agissait de champs définis dans le type courant. Prenons l’exemple d’un Point un peu particulier, auquel nous voudrions ajouter un label :

type LabelPoint struct { Point label string}

Nous pourrons définir ensuite un LabelPoint par :

lblPoint := LabelPoint(Point{3, 5}, "Mon point")

Sur cette variable, nous pourrons appliquer toutes les méthodes éventuellement définies pour LabelPoint, mais également celles de Point qui s’appliqueront sur les premiers champs enfouis :

lblPoint.Translate(2, 2) fmt.Println(lblPoint.ToString())

Un problème qui peut survenir est l’utilisation d’un nom de champ qui serait déjà utilisé dans une structure enfouie. Par exemple, si dans notre type LabelPoint au lieu de nom-mer notre champ label nous l’avions appelé x, que serait-il advenu, sachant que le type Point contient déjà un champ x ? Le champ de LabelPoint va masquer le champ enfoui qui sera lui accessible en le préfixant par le type Point (à condition que les deux champs soient visibles).

Pour simplifier la compréhension de ce phénomène et nous abstraire des problèmes de visibilité, nous allons repartir sur un exemple complet faisant intervenir les types Point et LabelPoint, définis au sein du même paquetage :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 53

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

01: package main 02: 03: import "fmt" 04: 05: type Point struct 06: { 07: x, y int 08: } 09: 10: type LabelPoint struct 11: { 12: Point 13: x int 14: } 15: 16: func main() { 17: p := LabelPoint{Point{5, 2}, 10} 18: fmt.Println(p.x) 19: fmt.Println(p.Point.x) 20: }

Ici, lorsque nous affichons la valeur de p.x en ligne 18, nous obtenons la valeur 10 : le champ x de LabelPoint a bien masqué le champ x issu du type Point. Pour accéder à ce champ, en ligne 19 nous utilisons p.Point.x.

Si un type enfouit plusieurs types possédant au moins un champ de même nom, il faudra préfixer ces champs par le nom du type souhaité pour pouvoir compiler.

2.3 HéritageL’héritage n’existe pas en Go ! Présenté comme l’un des

principaux points forts du modèle orienté objet, l’héritage dévient difficile à maintenir dans de gros projets (et de nombreux développeurs l’utilisent à mauvais escient). En Go, vous ne pourrez utiliser que la composition de classes et l’ « enfouissement » que nous avons vu précédemment, et vous obtiendrez un équivalent d’héritage (et même éventuellement d’héritage multiple). La surcharge de méthodes se fera par « masquage » des méthodes de la classe mère en les typant pour la classe fille.

Voici un exemple utilisant la classe Point déjà définie et implémentant la classe LabelPoint dans un fichier LabelPoint/LabelPoint.go :

01: package LabelPoint 02: 03: import "poo/Point" 04: 05: type LabelPoint struct { 06: Point.Point 07: label string 08: } 09: 10: func (p LabelPoint) GetLabel() string { 11: return p.label 12: } 13: 14: func (p *LabelPoint) SetLabel(value string) {15: p.label = value 16: }17:

18: func New(x_val, y_val int, label_val string) *LabelPoint { 19: return &LabelPoint{*Point.New(x_val, y_val), label_val} 20: } 21: 22: func (p LabelPoint) ToString() string { 23: return p.Point.ToString() + " - " + p.GetLabel() 24: }

La définition du type LabelPoint nécessite l’import de Point (ligne 3) pour définir les champs x et y (ligne 6) avant d’y ajouter le champ label (ligne 7). La définition de méthodes spécifiques au type LabelPoint se fait naturellement en indiquant la contrainte d’appel sur ce type de variable (lignes 10 à 12, 14 à 16 et 22 à 24). La dernière méthode, ToString(), est une ré-écriture de la méthode ToString() de Point et elle surcharge donc celle-ci. Néanmoins, nous pouvons utiliser la méthode issue de la « classe mère » en indiquant que ToString() s’appliquera au champ de type Point enfoui dans LabelPoint (ligne 23). La fonction New() des lignes 18 à 20 est le « constructeur » de notre type et il utilise lui-même le constructeur de Point. Ce dernier renvoyant un pointeur sur le type Point, nous préfixons l’appel par une * pour obtenir la valeur pointée.

Pour l’utilisation de cette classe, il faudra réécrire le main :

01: package main 02: 03: import ( 04: "fmt" 05: "poo/Point" 06: "poo/LabelPoint" 07: ) 08: 09: func main() { 10: p := Point.New(5, 2) 11: p_label := LabelPoint.New(1, 3, "Mon Point") 12: fmt.Println(p.ToString()) 13: fmt.Println(p_label.ToString()) 14: }

Ici, deux variables de type Point et LabelPoint sont créées (respectivement en lignes 10 et 11), puis la mé-thode ToString() leur est appliquée. Le résultat est bien celui attendu avec l’affichage de la ligne 12, qui correspond à la définition de la méthode dans Point.go et l’affichage de la ligne 13, qui fait bien appel à la méthode ToString() de Point avant de compléter l’information par le champ label de LabelPoint.

Vous avez pu voir que la structuration du code en types permet d’obtenir un code souple, réutilisable, équivalent de l’héritage... qui n’est bien sûr pas de l’héritage !

2.4 PolymorphismeLe polymorphisme s’implémente en Go grâce aux in-

terfaces que nous avons déjà utilisées dans des articles précédents. Attention, encore une fois, il ne s’agit pas d’un

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com54

Pour aLLer PLus LoiN La ProGraMMatioN orieNtée objet eN Go

polymorphisme dans les règles de l’art. Comme avec l’hé-ritage, on ne peut obtenir que quelque chose s’approchant du polymorphisme traditionnel.

Supposons que nous possédions plusieurs classes Cat et Dog. Ces classes possèdent une méthode Cry() qui in-dique le cri de l’animal (ce qui nous permet de remarquer que ces deux « classes » découlent d’une « classe » géné-rique Animal). Animal sera définie en tant qu’interface et contiendra la signature de la méthode Cry().

En Go, par convention, les noms d’interfaces se termi-nent par « er » et désignent une action d’après un verbe. Notre interface Animal s’appellera donc en fait Cryer. Pour simplifier l’application de notre exemple, tout le code se trouvera dans un même fichier :

01: package main 02: 03: import "fmt" 04: 05: type Cryer interface { 06: Cry() string 07: } 08: 09: 10: type Cat struct { 11: } 12: 13: func (c Cat) Cry() string { 14: return "Miaou" 15: } 16: 17: 18: type Dog struct { 19: } 20: 21: func (d Dog) Cry() string { 22: return "Ouaf" 23: } 24: 25: 26: func GenericCry(a Cryer) string { 27: return "Générique -> " + a.Cry() 28: } 29: 30: func main() { 31: var cat Cat 32: var dog Dog 33: fmt.Println("Cri du chat : " + cat.Cry()) 34: fmt.Println("Cri du chien : " + dog.Cry()) 35: fmt.Println("Cri du chat (générique) : " + GenericCry(cat)) 36: fmt.Println("Cri du chien (générique) : " + GenericCry(dog)) 37: }

Dans les lignes 5 à 7, nous définissons l’interface Cryer qui ne contient qu’une seule fonction Cry() qui ne prend pas d’argument et renvoie une chaîne de caractères. Les lignes 10 à 15, puis 18 à 23, permettent ensuite de dé-finir les deux types Cat et Dog, ainsi que leur fonction/méthode Cry(). Vous noterez que dans la définition de ces types, aucune mention à Cryer n’est faite : il s’agit de « classes » sans attribut. Mais où est donc l’intérêt d’avoir défini une interface ? La fonction GenericCry() des lignes 26 à 28 utilise un paramètre de type Cryer : cette fonction pourra ainsi être appelée en lui passant

en paramètre un élément de type Cat ou de type Dog. C’est ce qui est illustré par les lignes 35 et 36 appartenant à la fonction principale.

2.5 Classes abstraites et interfacesNous avons vu le rôle des interfaces qui permettent d’inté-

grer un type générique avec interface{}, ou bien de simuler un comportement de polymorphisme. Les interfaces en Go ne sont donc pas les interfaces de la terminologie orientée objet.

Dans l’exemple précédent portant sur le polymorphisme, le type Cryer est une interface, mais il peut être vu comme une sorte de classe abstraite : la méthode qu’il déclare n’est pas définie. Ainsi, le code suivant provoquera une erreur lors de l’appel :

var cry Cryercry.Cry()

Au niveau de l’enfouissement, les interfaces se comportent comme des types : une interface ou un type peut contenir une ou plusieurs interface(s). Par exemple, en repartant de l’interface Cryer, nous pourrions écrire une interface Earer, puis une interface Communicater regroupant les deux précédentes :

type Cryer interface { Cry() string }

type Earer interface { Ear() string }

type Communicater interface { Cryer Earer }

2.6 Traitement des erreurs par exceptions

La gestion des erreurs sera vue en détail dans le prochain article. Par rapport au traitement classique par exceptions en programmation orientée objet, sachez que Go propose un tel mécanisme, même s’il n’est pas recommandé de l’utiliser dans les conventions de codage.

ConclusionBien que j’aie laissé planer le doute depuis le début de

l’article, vous l’aurez deviné : Go n’est pas un langage orienté objet ! Mais rien ne nous empêche d’utiliser ses méthodes de développement de manière relativement simple et transparente, à la façon POO. De plus, à la panoplie de mécanismes proposés par Go, on peut ajouter le fait qu’il utilise un ramasse-miettes... On peut donc faire « comme si » il était orienté objet et tant pis pour les puristes (extrémistes ?) qui passeront leur chemin.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 55

Pour aLLer PLus LoiN

La GesTIon Des erreUrspar Tristan Colombo

Un code ne s’exécute pas toujours correctement. Cela est d’autant plus vrai lorsque l’utilisateur peut saisir des données ou que des données sont lues dans des sources externes. Il faut alors indiquer proprement qu’une erreur est apparue.

1 assurer l’exécution d’un code même en cas d’erreur

Lorsque l’on exécute du code, il est possible qu’une erreur survienne mais que l’on souhaite tout de même exécuter quelques instructions avant de renvoyer l’erreur. Cela se produit typiquement lorsque l’on manipule des fichiers : en cas d’erreur, on va vouloir fermer proprement le fichier avant de renvoyer un message. Pour indiquer à Go le code à exécuter avant de renvoyer un résultat (return), il faudra utiliser l’instruction defer.

Nous n’avons pas encore vu comment manipuler les fichiers en Go. Voici donc comment utiliser defer dans une syn-taxe mi-Go, mi-algorithmique :

01: func readFile(filename string) { 02: defer fermeture du fichier03: 04: ouverture du fichier05: lecture, traitement et affichage des données06: }

Deux cas se présentent : soit le fi-chier est ouvert puis lu sans problème et l’instruction defer est exécutée (et donc le fichier est fermé), soit le fichier est ouvert puis une donnée provoque la fin de lecture du fichier et l’instruc-tion defer est exécutée. Quoiqu’il advienne, les instructions du defer seront donc exécutées. Si plusieurs

traitements defer sont spécifiés dans une fonction, ils seront exécutés dans l’ordre inverse de leur définition (LIFO : Last In First Out).

Essayons d’appliquer cette instruction dans le cadre d’un exemple : imaginons une fonction effectuant une opération sur les éléments d’un slice (une multipli-cation par exemple). Si l’un des éléments est supérieur à mille, nous arrêtons les calculs et les valeurs du slice sont mises à 0. Dans un premier temps, nous nous contenterons d’afficher un message :

01: package main 02: 03: import "fmt" 04: 05: func calc(s []int) { 06: defer fmt.Println("Passage dans defer")07: 08: for index, value := range s { 09: if value > 1000 { 10: break 11: } 12: s[index] = 2 * value 13: } 14: } 15: 16: func main() { 17: s := []int{1, 3, 2500, 4} 18: calc(s) 19: fmt.Println(s) 20: }

La fonction calc() des lignes 5 à 14 comporte deux parties : la déclaration des traitements à effectuer avant la sortie de la fonction (ligne 6) et le traitement courant de la fonction (lignes 8 à 13). Dans ce traitement, on parcourt le slice passé en paramètre tant que les

valeurs sont inférieures ou égales à mille (lignes 9 à 11) et on les multiplie par deux (ligne 12).

Dans la fonction principale, nous déclarons un slice d’entiers et nous appelons la fonction calc(). D’après le slice que nous avons passé en pa-ramètre, les deux premiers éléments (1 et 3) seront traités et le troisième élément (2500) provoquera un appel à break. Avant de sortir de la fonction, un appel à defer (ligne 6) sera fait et l’on affichera le message « Passage dans defer ».

Si nous souhaitons ré-initialiser le slice, il faudra que l’instruction defer exécute une fonction (une fermeture). Faites attention à la syntaxe : la fonc-tion doit être définie, mais également appelée ! Il faut donc penser à ajouter les caractères () :

06: defer func () { 07: for index, _ := range s { 08: s[index] = 0 09: } 10: }()

Mais ce code pose problème. Pas-sons outre le fait que nous aurions pu initialiser le slice directement dans le premier test sur la valeur mille, il ne s’agit que d’un exemple que nous ferons évoluer au cours de l’article. Le problème provient du fait que les instructions du defer étant toujours exécutées, nous obtiendrons dans tous les cas un slice ne contenant que des zéros... Il faut donc une autre méthode de traitement des erreurs.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com56

Pour aLLer PLus LoiN La GestioN des erreurs

2 Gérer les exceptions

La fonction panic() permet de signaler une erreur – on dit encore « lever une exception ». Un message ou une valeur pourra être passé(e) en paramètre et la fonction recover() permettra de récupérer l’exception. Contrairement à Java, l’usage des exceptions n’est pas encouragé en Go. Dans les bonnes pra-tiques, une exception ne doit être levée que si l’erreur est vraiment bloquante et si elle doit être récupérée, il faut que les instructions de récupération soient le plus proche de la source de l’erreur (ce qui s’oppose par exemple complète-ment à la philosophie de remontée des exceptions dans une classe de gestion de toutes les erreurs). Si une erreur n’est pas bloquante, la plupart du temps, elles renvoient une valeur supplémentaire qui est une valeur d’erreur.

2.1 renvoyer une erreurLe type pré-défini error permet de

renvoyer une erreur en utilisant un format « universel ». En effet, sinon, en fonction des habitudes, certains développeurs utiliseraient des codes d’erreurs, d’autres seulement des chaînes de caractères ou encore des map, etc. Au lieu de faire renvoyer n valeurs par une fonction, nous renverrons n + 1 va-leurs, où la valeur supplémentaire est de type error (l’utilisation de ce type n’est pas obligatoire, il s’agit seulement d’une bonne pratique). Si nous appliquons ce mécanisme à l’exemple précédent, nous obtenons :

01: package main 02: 03: import "fmt" 04: 05: func calc(s []int) error {06: for index, value := range s { 07: if value > 1000 { 08: return fmt.Errorf("Value > 1000 : %d\n", value)09: } 10: s[index] = 2 * value 11: } 12:13: return nil 14: } 15:

16: func main() { 17: s := []int{1, 3, 2500, 4} 18: 19: if err := calc(s); err != nil { 20: fmt.Print(err) 21: for index, _ := range s { 22: s[index] = 0 23: } 24: }25: 26: fmt.Println(s) 27: }

Auparavant, la fonction calc() ne renvoyait aucun résultat (n = 0). Nous renverrons désormais une erreur, donc une seule valeur de type error (ligne 5). Lorsqu’une valeur du slice dépasse la valeur maximale (ligne 7), nous retournons une erreur en utilisant la fonction Errorf() du paquetage fmt (ligne 8). Cette fonction formate une chaîne de caractères et renvoie un objet error. En implémentant ce méca-nisme, l’appel de la fonction a forcément été modifié, puisque calc() retourne maintenant une valeur que nous allons tester pour savoir si une erreur s’est produite ou non (ligne 19). En cas d’erreur (err différent de nil), nous affichons le message d’erreur (ligne 20) et nous remettons les valeurs du slice à zéro (lignes 21 à 23). Si cette erreur est considérée comme bloquante, nous aurions pu utiliser les exceptions.

2.2 Lever et récupérer une exception

Pour récapituler, nous avons vu que pour lever une exception il fallait employer la fonction panic(), que pour récupérer une exception il fallait utili-ser recover() et qu’il était préférable de traiter les erreurs au plus près de la source d’émission. En récupérant l’exception dans un defer, nous pou-vons parvenir à ce résultat comme le montre l’application de cette technique sur l’exemple que nous modifions depuis le début de cet article :

01: package main 02: 03: import "fmt" 04: 05: func calc(s []int) { 06: defer func () { 07: if e := recover(); e != nil {

08: fmt.Print(e) 09: for index, _ := range s { 10: s[index] = 0 11: } 12: } 13: }()14: 15: for index, value := range s { 16: if value > 1000 { 17: panic(fmt.Sprintf("Value > 1000 : %d\n", value))18: } 19: s[index] = 2 * value 20: } 21: } 22: 23: func main() { 24: s := []int{1, 3, 2500, 4} 25: 26: calc(s) 27: 28: fmt.Println(s) 29: }

Lors de la détection d’une erreur, nous lançons une exception à l’aide de panic() en ligne 17. Cette fonction prend en pa-ramètre un message qui sera transmis en utilisant la fonction Sprintf() pour construire une chaîne de caractères formatée. Cette exception sera récupérée dans le bloc defer (lignes 6 à 13) grâce à l’appel de recover(). Nous pourrons avoir accès au message de l’exception en affichant la variable stockant le résultat de recover(). Il faut savoir que la signature de cette fonction est : func recover() interface{}. Elle renvoie donc n’importe quel type d’élé-ment : nous avions transmis une chaîne de caractères dans le panic(), il s’agira donc d’une chaîne de caractères, mais rien ne vous empêche d’utiliser un entier ou autre (un type error par exemple...).

En ligne 8, nous affichons le message d’erreur avant de remettre les valeurs du slice à zéro (lignes 9 à 11). Mais est-il bien pertinent d’afficher un mes-sage d’erreur alors que le programme continue de s’exécuter ?

3 Les fichiers de log

Plutôt que d’afficher des messages d’erreur à l’écran, nous pouvons les stocker dans un fichier de log grâce au paquetage de même nom. Par défaut, les fonctions de log afficheront leurs

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 57

Pour aLLer PLus LoiN La GestioN des erreurs

messages à l’écran, mais grâce à la fonction SetOutput() nous pourrons préciser que nous désirons une sau-vegarde des messages dans un fichier.

Le paquetage log propose de nom-breuses fonctions pour la gestion des messages d’erreur :

- log.Fatal(v ...interface{}) pour afficher les données du slice v à l’aide d’un Print(v), puis quitter le programme avec un code d’erreur 1 (code récupérable dans le shell par $?) ;

- log.Fatalf(format string, v ...interface) pour un équivalent de log.Fatal(), mais avec formatage ;

- log.Panic(v ...interface) équivalent d’un appel à Print(v), suivi d’un appel à Panic() ;

- log.Panicf(format string, v ...interface) pour un équiva-lent de log.Panic(), mais avec formatage ;

- log.Setflags(flag int) pour paramétrer l’affichage à l’aide de constantes fournies par le paque-tage. Par défaut, log affiche la date et l’heure d’appel de la fonction, mais il est possible de paramétrer cet affichage grâce à cette fonction. On peut même désactiver ces in-formations en passant la valeur 0 en paramètre ;

- log.SetPrefix(prefix string) permet de définir le préfixe lors de l’affichage des lignes de log.

Utilisons les fonctions de ce paquetage dans notre exemple :

01: package main 02: 03: import ( 04: "fmt" 05: "log"06: ) 07: 08: func init() { 09: log.SetFlags(0) 10: log.SetPrefix("[GLMF HS] ") 11: }12: 13: func calc(s []int) { 14: defer func () { 15: if e := recover(); e != nil { 16: for index, _ := range s { 17: s[index] = 0

18: } 19: } 20: }() 21: 22: for index, value := range s { 23: if value > 1000 { 24: log.Panicf("Value > 1000 : %d\n", value)25: } 26: s[index] = 2 * value 27: } 28: } 29: 30: func main() { 31: s := []int{1, 3, 2500, 4} 32: 33: calc(s) 34: 35: fmt.Println(s) 36: }

Le paquetage log doit bien sûr être importé (ligne 5). Nous ajoutons une fonction init() qui sera exécutée avant le main() et qui permettra de fixer les options d’affichage du log : pas d’indication de date ni d’heure et ajout du préfixe « [GLMF HS] ». Le reste du code reste inchangé, si ce n’est l’appel à panic() qui se fera désormais depuis la fonction Panicf() de log (ligne 24). L’affichage de l’erreur produira le ré-sultat suivant :

[GLMF HS] Value > 1000 : 2500

Si nous n’avions pas supprimé l’horo-datage par SetFlags(0), nous aurions obtenu :

[GLMF HS] 2012/09/21 16:26:35 Value > 1000 : 2500

Nous avons réalisé ici un log à l’écran, mais notre objectif de départ était justement de ne plus rien afficher sur la sortie standard et de stocker les messages dans un fichier. Il faut alors ouvrir un fichier en mode ajout et indiquer que les informations de log devront y être inscrites :

01: package main 02: 03: import ( 04: "fmt" 05: "log" 06: "os"07: ) 08: 09: var LogFile *os.File10: 11: func init() {

12: LogFile, _ = os.OpenFile("mylogfile.log", os.O_WRONLY | os.O_APPEND | os.O_CREATE,13: 0666) 14: log.SetOutput(LogFile)15: log.SetFlags(0) 16: log.SetPrefix("[GLMF HS] ") 17: } 18: 19: func calc(s []int) {

... 34: } 35: 36: func main() { 37: defer LogFile.Close()38: 39: s := []int{1, 3, 2500, 4} 40: 41: calc(s) 42: 43: fmt.Println(s) 44: }

C’est le module os, chargé en ligne 5, qui permet de gérer l’ouverture du fichier. On indique le nom du fichier (mylogfile.log) lors de la création de la variable descriptive du fichier (lignes 12 et 13). Cette variable a été déclarée en tant que variable globale en ligne 9. La fonction calc() des lignes 19 à 34 reste inchangée. Enfin, dans la fonction main(), en ligne 37 nous ajoutons une instruction defer pour s’assurer que le fichier est bien fermé proprement.

ConclusionEn Go, il est préconisé d’utiliser des

valeurs de retour pour indiquer l’appa-rition d’erreurs lors de l’exécution d’une fonction plutôt que des exceptions. Un mécanisme de gestion de ces exceptions est toutefois présent et peut donc être utilisé suivant les envies du développeur. Enfin, une instruction permet de s’as-surer de l’exécution de lignes de code avant de sortir d’une fonction. Ceci est particulièrement utile dans la gestion des fichiers. À ce propos, si le dernier exemple vous a laissé sur votre faim et a attisé votre curiosité sur le traitement des fichiers en Go, sachez que c’était tout à fait voulu ! Si vous souhaitez obtenir plus d’informations... je vous invite à lire l’article suivant !

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com58

Pour aLLer PLus LoiN traiteMeNt des fichiers

TraITeMenT Des fICHIerspar Tristan Colombo

Que l’on souhaite conserver des données après l’exécution d’un programme ou obtenir des informations issues d’un autre programme ou d’un quelconque dispositif électronique, la manière la plus simple de faire sera de passer par des fichiers.

Les étapes d’ouverture d’un fichier sont les mêmes que dans la plupart des langages :

- Ouverture du fichier en fonction de son nom et du type d’accès souhaité (lecture, écriture ou ajout) ;

- Lecture ou écriture dans le fichier ;

- Fermeture du fichier.

Ces étapes de base sont valides quel que soit le type de fichier que l’on souhaite manipuler. Après, en fonction du type de fichier, certains paquetages fourniront des outils plus ou moins pertinents permettant de faciliter le travail du développeur.

Nous n’étudierons dans cet article que certains types de fichiers : les fichiers les plus courants, c’est-à-dire les fichiers textes, puis des fichiers structurés avec les formats JSON et XML, des fichiers dans un format spécifique à Go (fichiers binaires Go d’extension gob), et pour finir, nous verrons comment manipuler les fichiers d’archives zIP.

1 Les fichiers textesPlusieurs paquetages permettent de manipuler les

fichiers textes : le paquetage os, que nous avons déjà ren-contré dans le cadre de l’article sur la gestion des erreurs, le paquetage bufio permettant de gérer des buffers de runes ou d’octets et le paquetage ioutil, sous-paquetage de io, permettant de simplifier les accès au détriment de la richesse fonctionnelle.

1.1 Le paquetage osLe paquetage os n’est pas dédié à cela, mais il permet

de manipuler relativement simplement les fichiers. Nous allons voir comment lire et comment écrire dans un fichier.

1.1.1 Lecture d’un fichieros propose deux méthodes d’ouverture d’un fichier :

Open() et OpenFile(). La première méthode est une simplification de la seconde destinée uniquement à la

lecture de fichiers. Comme c’est ce que nous souhaitons faire ici, pour lire et afficher le contenu d’un fichier monFichier.txt, c’est Open() que nous allons utiliser :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: "io" 07: ) 08: 09: func main() { 10: fic, err := os.Open("monFichier.txt") 11: defer fic.Close() 12: 13: if err != nil { 14: fmt.Println("Ouverture de fichier impossible") 15: fmt.Println(err) 16: os.Exit(1) 17: } 18: 19: data := make([]byte, 10) 20: for { 21: n, err := fic.Read(data) 22: if err == io.EOF { 23: fmt.Println("Fin du fichier") 24: break 25: } 26: for i := 0; i < n; i++ { 27: fmt.Print(string(data[i])) 28: } 29: } 30: }

Nous avons bien sûr besoin d’importer le paquetage os, qui fournit les méthodes d’accès au fichier (ligne 5) et le paquetage io qui contient la définition de la constante EOF permettant de savoir quand le fichier a été entièrement lu (ligne 6). La ligne 10 permet d’ouvrir le fichier monFichier.txt en lecture seule et de recueillir dans les variables fic et err le pointeur de fichier et l’éventuelle erreur. Immédiatement après, en ligne 11, nous déclarons un traitement defer pour refermer proprement le fichier à la sortie de la fonction main().

Les lignes 13 à 17 permettent de traiter une éventuelle erreur : si la variable err n’est pas égale à nil, c’est qu’un problème est survenu, empêchant l’ouverture du fichier. Nous affichons alors

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 59

Pour aLLer PLus LoiN traiteMeNt des fichiers

un message d’erreur (ligne 14), suivi du message standard fourni par la variable err (ligne 15), puis nous quittons le programme avec un code d’erreur 1 grâce à la fonction os.Exit().

Si le fichier a été correctement ouvert, nous pouvons passer à sa lecture dans les lignes 19 à 29. Pour cela, nous créons un slice data de dix octets (vous pouvez choisir la taille que vous souhaitez). C’est ce slice qui contiendra les données lues dans la boucle for infinie des lignes 20 à 29. Dans cette boucle, nous commençons par lire dix octets dans le fichier pointé par fic et nous récupérons dans n le nombre d’octets lus, dans err une éventuelle erreur et dans data les octets qui ont été lus. Si err prend la valeur io.EOF, c’est que nous avons atteint la fin du fichier (test en ligne 22). Nous affichons alors un message (ligne 23) avant de sortir de la boucle grâce à un break (ligne 24). Pour afficher les données, nous effectuons une boucle sur le nombre de caractères lus et nous les affichons caractère à caractère, en faisant attention à bien convertir les octets en chaîne de caractères (ligne 27).

Cette méthode fonctionne correctement s’il n’y a pas de caractère accentué codé en UTF-8... donc sur deux octets ! Comme nous affichons les caractères en convertissant octet par octet, nous obtiendrons deux caractères « erronés ». L’intérêt de cette méthode est de limiter les opérations sur le slice. En effet, pour corriger le problème nous pourrions penser qu’il suffit d’afficher directement la variable data convertie :

19: data := make([]byte, 10) 20: for { 21: _, err := fic.Read(data)22: if err == io.EOF { 23: fmt.Println("Fin du fichier") 24: break 25: } 26: fmt.Print(string(data))27: }

L’idée peut paraître bonne... mais nous affichons à chaque itération l’intégralité du slice data, ce qui signifie que si lors de la lecture de la dernière partie du fichier moins de dix octets sont lus, nous afficherons les derniers caractères parasites de la ligne précédente. Prenons pour exemple la ligne suivante, issue de la fin d’un fichier :

Test pour GLMF

Cette ligne est composée de 14 caractères, soit 10 + 4. Donc, à l’itération n-1, la variable data contiendra « Test pour » et à la dernière itération n, elle contiendra « GLMF pour ». Le mot « pour » est un parasite issu de la lecture précédente. Pour corriger ce problème, il faut soit ré-allouer le slice data à chaque itération en intégrant la ligne 19 dans le corps de la boucle, soit il faut ré-initialiser les valeurs du slice à chaque itération en ajoutant les lignes suivantes après la ligne 20 :

21: for i := range data { 22: data[i] = 0 23: }

1.1.2 Écriture dans un fichierPour écrire dans un fichier, nous allons utiliser l’ouver-

ture de fichier avec la méthode OpenFile(). Cette méthode retourne les mêmes types de valeur que la méthode Open(). Au niveau des paramètres, elle attend un nom, un type d’ou-verture sous la forme d’un entier et les droits à utiliser en cas de création. Les droits sont indiqués sous la forme octale standard, 0 suivi de trois entiers qualifiant les droits ugo (u pour utilisateur, g pour groupe et o pour autres). Les entiers sont la somme des droits suivants : 1 pour exécution, 2 pour écriture et 4 pour lecture. Ainsi, un fichier ayant la permission 0644 sera accessible en lecture/écriture pour l’utilisateur et seulement en lecture pour les autres. Le type d’ouverture du fichier est déterminé à l’aide de constantes qui peuvent être « ajoutées » grâce à l’opérateur « ou » bit à bit |. Voici quelques-unes de ces constantes et leur signification :

- O_RDONLY : ouverture en lecture ;- O_WRONLY : ouverture en écriture ;- O_APPEND : ajout des données en fin de fichier si

celui-ci existe ;- O_CREATE : création d’un nouveau fichier s’il n’existe pas ;- O_EXCL : le fichier doit être créé et ne pas exister (à

utiliser avec O_EXCL).

La méthode Create() est également disponible pour une ouverture en lecture, mais elle ne permet pas de gérer les droits. L’écriture d’une chaîne de caractères sera simplifiée par la méthode WriteString() qui est une réécriture de la méthode Write(), mais qui accepte en paramètre une chaîne de caractères plutôt qu’un slice d’octets. Pour continuer à explorer un peu plus le langage Go, le nom de fichier de l’exemple suivant sera récupéré directement dans la ligne de commandes :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: ) 07: 08: func main() { 09: if len(os.Args) != 2 { 10: fmt.Println("Syntaxe: ecrire <nom_fichier>") 11: os.Exit(2) 12: }13: 14: fic, err := os.OpenFile(os.Args[1], os.O_CREATE | os.O_WRONLY, 0666)15: defer fic.Close() 16: 17: if err != nil { 18: fmt.Println("Ouverture de fichier impossible") 19: fmt.Println(err) 20: os.Exit(1) 21: } 22: 23: fic.WriteString("Écriture dans le fichier")24: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com60

Pour aLLer PLus LoiN traiteMeNt des fichiers

La structure générale du code est très semblable à celle utilisée pour la lecture. Les seules modifications sont indiquées en rouge. Comme nous récupérons le nom du fichier à créer depuis la ligne de commandes, il faut tester le nombre d’arguments transmis. Ces arguments se trouvent dans la variable os.Args et nous testons donc la longueur de ce tableau où le premier élément correspond au nom du programme, puis os.Args[n] correspond au nième paramètre (lignes 9 à 12).

Si le programme est appelé avec trop ou trop peu de paramètres, nous affichons un message d’erreur (ligne 10), puis nous quittons le programme avec un code d’erreur 2 (ligne 11). L’ouverture du fichier a lieu en ligne 14 : on in-dique comme nom os.Args[1], le fichier devra être créé s’il n’existe pas et sera ouvert en écriture (os.O_CREATE | os.O_WRONLY) et enfin, les droits du fichier seront 666 (lecture et écriture pour tous). Pour écrire dans le fichier, comme dit précédemment, c’est la méthode WriteString() qui est utilisée en ligne 23. Faites toutefois attention à un détail : si le fichier existe déjà et que les données sont plus importantes que celles que vous allez écrire, les données qui ne seront pas écrasées seront toujours visibles... Ce mécanisme peut être intéressant, mais si vous voulez re-partir d’un fichier vierge, pensez à effacer l’ancien fichier auparavant ou utilisez la méthode Create().

Le paquetage os fournit également d’autres outils plus génériques permettant de travailler sur les fichiers : Chdir() pour changer le répertoire courant de travail, Chmod() pour changer les droits d’un fichier, Remove() pour supprimer un fichier, etc. N’oubliez pas que Go est multiplateforme et qu’à ce titre, si vous souhaitez qu’il le reste, vous ne devez pas employer directement d’appel à des fonctions système. Passez toujours par les fonctions fournies dans les paquetages.

1.2 Le paquetage bufioEn utilisant ce paquetage, l’ouverture des fichiers se fera

toujours grâce aux fonctions de os que nous avons vues précédemment. Nous utiliserons ensuite des objets Reader ou Writer fournis par le paquetage pour lire ou écrire plus simplement dans les fichiers.

1.2.1 Lecture d’un fichierLa lecture d’un fichier caractère à caractère sera grande-

ment facilitée par l’utilisation de la méthode ReadRune() :

01: package main 02: 03: import ( 04: "fmt" 05: "os" 06: "io" 07: "bufio"08: )

09: 10: func main() { 11: fic, err := os.Open("monFichier.txt") 12: r := bufio.NewReader(fic)13: defer fic.Close() 14: 15: if err != nil { 16: fmt.Println("Ouverture de fichier impossible") 17: fmt.Println(err) 18: os.Exit(1) 19: } 20: 21: for { 22: car, _, err := r.ReadRune() 23: if err == io.EOF { 24: break 25: } 26: fmt.Print(string(car)) 27: }28: }

Les différences essentielles par rapport aux exemples précédents sont toujours signalées en rouge. La création de l’objet de lecture se fait en ligne 12 par appel de NewReader() sur la variable fic. La boucle de lecture des lignes 21 à 27 fait maintenant appel à ReadRune(), qui renvoie le caractère lu dans car, le nombre d’octets composant ce caractère (nous ne conservons pas cette information), et éventuellement une erreur qui est récupérée dans err. La fin de lecture se gère toujours à l’aide de io.EOF (ligne 23) et pour afficher notre caractère, il faudra là encore penser à le convertir en string (ligne 26).

bufio propose aussi de pouvoir revenir en arrière dans le fichier en « oubliant » un caractère lu à l’aide de UnreadRune(). Par exemple, si vous voulez lire un fichier avec un retour en arrière tous les trois caractères, il faudra taper :

a := 0for { car, _, err := r.ReadRune() if err == io.EOF { break } fmt.Print(string(car)) if a++; a % 3 == 0 { r.UnreadRune() }}

1.2.2 Écriture dans un fichierL’écriture apportera peu de changements pour les chaînes de

caractères avec WriteString(), mais vous pourrez également écrire des runes caractère à caractère avec WriteRune() ou encore des octets avec WriteByte(). Voici le code incomplet montrant l’utilisation de NewWriter() :

// ... fic, err := os.OpenFile("monFichier.txt", O_CREATE | O_WRONLY, 0666) w := bufio.NewWriter(fic) // ... w.WriteString("Écriture dans le fichier")// ...

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 61

Pour aLLer PLus LoiN traiteMeNt des fichiers

1.3 Le paquetage ioutilLe paquetage ioutil simplifie à l’extrême les accès fichiers ;

tout se fera en un appel : ouverture, lecture ou écriture d’un tableau d’octets, puis fermeture.

1.3.1 Lecture d’un fichierPour la lecture, nous divisons par deux le nombre de lignes,

mais attention : tout le fichier se retrouve en mémoire dans un tableau d’octets !

01: import ( 02: "fmt" 03: "io/ioutil"04: ) 05: 06: func main() { 07: data, err := ioutil.ReadFile("monFichier.txt")08: 09: if err != nil { 10: panic(err) 11: } 12: 13: fmt.Print(string(data)) 14: }

Il n’y a pas grand-chose à expliquer sur ce bout de code : les données du fichier sont stockées dans la variable data (ligne 7), qui sera utilisée pour l’affichage en ligne 13. Pour accentuer la diminution du nombre de lignes (et pour changer un peu), j’ai utilisé ici la fonction panic() pour signaler une erreur et sortir du programme.

1.3.2 Écriture dans un fichierLe code est encore plus court pour l’écriture dans un

fichier. Cette fois-ci, le fichier est automatiquement effacé s’il existe déjà :

01: package main 02: 03: import "io/ioutil" 04: 05: func main() { 06: data := []byte("Écriture dans un fichier") 07: err := ioutil.WriteFile("monFichier.txt", data, 0666) 08: 09: if err != nil { 10: panic(err) 11: } 12: }

2 Les fichiers xMLPour commencer, pour ceux d’entre vous qui l’auraient peut-

être oublié, voici une définition du format XML (eXtensible Markup Langage). Il s’agit d’un langage à balises ou encore à tags, les termes étant différents, mais désignant le même objet. Les tags sont représentés en encadrant des termes par < et > (<tag> est, par exemple, une balise/tag). Ces tags ne

sont pas figés et vous pouvez donc en créer un utilisant n’importe quel nom vous passant par la tête. Le but de ce langage étant de produire des documents structurés, les tags vont permettre de déterminer des blocs définissant des objets précis. Par exemple, un magazine contient un numéro et une date de parution. Pour décrire cet objet en XML, nous utiliserons trois tags et comme ceux-ci définissent des blocs (« magazine » contient « numero » et « date_parution »), à chaque tag « ouvrant » un bloc (noté <tag>), nous associerons un tag « fermant » (noté </tag>). Voici le code correspondant à notre exemple :

<magazine> <numero>63</numero> <date_parution>novembre/décembre 2012</date_parution></magazine>

Grâce à cette structure, nous pouvons définir un nombre infini de magazines pour les stocker par exemple dans une bibliothèque. Nous aurons alors un élément, appelé « racine », qui sera un tag contenant l’ensemble des tags de notre document XML :

<bibliotheque> <magazine> <numero>63</numero> <date_parution>novembre/décembre 2012</date_parution> </magazine> <magazine> ... </magazine> ...</bibliotheque>

Pour être complet, un document XML doit contenir des informations supplémentaires : le prologue. Cette section indique, sous la forme de deux lignes d’entête, que le document est un document XML, quelle version des spécifications XML il respecte, quel est l’encodage des caractères employé, quel est l’élément racine permettant de commencer la lecture du document et enfin, quelle est l’adresse de la grammaire ou DTD (Document Type Definition) définissant le document. Dans notre exemple, la grammaire précisera qu’un magazine contient un tag numero et un tag date_parution et qu’une bibliotheque peut contenir plusieurs magazine.

<?xml version="1.0" encoding="utf-8"?><!doctype bibliotheque system "biblio_grammaire.dtd">

Voici donc comment écrire et lire des fichiers en utilisant ce format et le paquetage encoding/xml.

2.1 Écriture dans un fichierPour l’écriture d’un document XML, la première des

choses à faire est de définir la ou les structure(s) Go équivalente(s) à la structure du document. Cette structure sera spécifique au XML. Elle contiendra le nom du nœud

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com62

Pour aLLer PLus LoiN traiteMeNt des fichiers

sous la forme d’un champ dénommé XMLName et de type Name. La valeur de ce champ sera donnée sous la forme `xml:"nom_du_nœud" .̀ Les champs suivants auront des valeurs de la forme :

- ̀ xml:"nom_du_nœud"` pour spécifier le nom d’un nœud ;- ̀ xml:""` pour indiquer que le nom du nœud est le même que celui du champ ;- `xml:"nom_du_nœud,attr"` pour spécifier le nom d’un attribut ;- ̀ xml:",attr"` pour indiquer que le nom d’un attribut est le même que celui

du champ ;

Pour indiquer un lien de parentalité, vous pouvez utiliser le caractère > dans le nom d’un champ. Par exemple, la chaîne de caractères ̀ xml:"parent>enfant"` définit la structure :

<parent> <enfant></enfant></parent>

La fonction MarshalIndent() sera ensuite appelée sur la structure afin de produire un document correctement indenté, auquel il suffira d’ajouter un entête qui est fourni par la constante Header :

01: package main 02: 03: import ( 04: "encoding/xml" 05: "io/ioutil" 06: ) 07: 08: type Magazine struct { 09: XMLName xml.Name `xml:"magazine"` 10: Numero int `xml:"numero"` 11: Date_parution string `xml:"date_parution"` 12: } 13: 14: type Bibliotheque struct { 15: XMLName xml.Name `xml:"bibliotheque"` 16: Magazine []Magazine 17: } 18: 19: func main() { 20: m1 := Magazine{Numero : 62, Date_parution : "novembre/décembre 2012"} 21: m2 := Magazine{Numero : 63, Date_parution : "janvier/février 2013"} 22: b := Bibliotheque{Magazine : []Magazine{m1, m2}} 23: 24: xmlData, _ := xml.MarshalIndent(b, "", " ") 25: 26: err := ioutil.WriteFile("data.xml", append([]byte(xml.Header), xmlData...), 0666) 27: 28: if err != nil { 29: panic(err) 30: } 31: }

Pour définir notre document XML, nous allons utiliser deux structures : Magazine dans les lignes 8 à 12 et Bibliotheque dans les lignes 14 à 17. Comme Bibliotheque peut contenir plusieurs Magazine, nous utilisons un champ de type slice en ligne 16. Il suffit ensuite de créer les différents éléments (lignes 20 à 22) et d’appeler la fonction MarshalIndent() pour obtenir un slice d’octets contenant les données. Cette fonction prend en paramètres les données struc-turées, une chaîne de caractères à ajouter en début de chaque nouvelle ligne, et une chaîne de caractères servant à l’indentation.

Les données utilisées lors de l’écriture dans le fichier, en ligne 26, prennent une forme un peu spéciale : nous commen-çons par convertir la chaîne fournie par xml.Header en slice d’octets grâce à []byte(xml.Header). Nous voulons en-suite agrandir notre tableau en collant à la fin de celui-ci les données de xmlData. Pour cela, nous utilisons la fonction append qui prend en paramètres un slice suivi d’une liste de valeurs à y ajouter. Or xmlData est déjà un slice... Pour que ces données soient utilisées comme autant de valeurs, nous post-fixons son nom par ..., ce qui donne append([]byte(xml.Header), xmlData...).

2.2 Lecture d’un fichierÀ la lecture du fichier, il faut rappeler

la structure des données. La fonc-tion Unmarshal() convertira ensuite les données lues en structures Go :

01: package main 02: 03: import ( 04: "encoding/xml" 05: "io/ioutil" 06: "fmt" 07: "strconv" 08: ) 09: 10: type Magazine struct { 11: XMLName xml.Name `xml:"magazine"` 12: Numero int `xml:"numero"` 13: Date_parution string `xml:"date_parution"` 14: } 15: 16: type Bibliotheque struct { 17: XMLName xml.Name `xml:"bibliotheque"` 18: Magazine []Magazine `xml:"magazine"`19: } 20: 21: func main() { 22: data, err := ioutil.ReadFile("data.xml") 23: 24: if err != nil { 25: panic(err) 26: } 27: 28: var b Bibliotheque 29: xml.Unmarshal(data, &b) 30: 31: for _, m := range b.Magazine { 32: fmt.Println("N." + strconv.Itoa(m.Numero) + " de " + m.Date_parution) 33: } 34: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 63

Pour aLLer PLus LoiN traiteMeNt des fichiers

Les structures sont rappelées dans les lignes 10 à 19. Notez l’ajout, signalé en rouge, de `xml:"magazine"` en ligne 18 : si le nom des nœuds n’est pas spécifié ici, vous ne pourrez pas récupérer les différentes entrées de type magazine. Nous lisons ensuite les données depuis le fichier (ligne 22), puis nous les convertissons en Biblio-theque (lignes 28 et 29) avant d’afficher chacun des magazines (lignes 31 à 33). La fonction strconv.Itoa() est utilisée pour convertir l’entier m.Numero en chaîne de caractères.

3 Les fichiers json

Le format JSON, créé par Douglas Crockford, est un format de données structurées. JSON signifie JavaScript Ob-ject Notation et un tel document est composé de deux types d’éléments structurels : des ensembles de paires nom/valeur, et des listes ordonnées de valeurs. Exemple d’écriture JSON :

{ tab : { val1 : 1, val2 : 2}, elt : ‘chaine’ ...}

La notation XML correspondante serait :

<tab> <val1>1</val1> <val2>2</val2></tab><elt>chaine</elt>

On peut ainsi obtenir des structures complexes qui restent simplement manipulables. Nous allons voir dans la suite comment utiliser ce format en Go grâce au paquetage encoding/json.

3.1 Écriture dans un fichier

Le paquetage encoding/json ne va nous servir qu’à encoder les données que nous souhaitons sauvegarder dans un fichier. La fonction Marshal() attend en paramètre un type structuré et renvoie

un tableau d’octets, ce qui simplifie le travail pour l’enregistrement, puisque les fonctions attendent justement un tableau d’octets ! En utilisant la structure de nœud magazine de l’exemple portant sur le XML, voici ce que donne le code :

01: package main 02: 03: import ( 04: "encoding/json" 05: "io/ioutil" 06: ) 07: 08: type Magazine struct { 09: Numero int 10: Date_parution string 11: } 12: 13: func main() { 14: m := Magazine{Numero : 62, Date_parution : "novembre/décembre 2012"} 15: data, _ := json.Marshal(m) 16: 17: err := ioutil.WriteFile("data.json", data, 0666) 18: 19: if err != nil { 20: panic(err) 21: } 22: }

Le type des données est défini dans les lignes 8 à 11. Faites attention à la visibilité de vos noms de champs dans la structure à traduire en JSON : avec une visibilité paquetage (noms commençant par une minuscule), ils seront invisibles pour Marshal() et ne se retrouveront pas dans le slice de sortie. Une variable de type Magazine est créée en ligne 14, en lui associant des valeurs de manière à être convertie en slice d’octets par la fonction Marshal() de la ligne 15. Cette fonction renvoie deux éléments : le tableau d’octets et une éventuelle erreur (que nous ignorons dans le cadre de cet exemple). L’écriture dans le fichier se fait en-suite très simplement à l’aide du paquetage ioutil (ligne 17). Après exécution de cet exemple, le contenu du fichier data.json est :

{"Numero":62,"Date_parution":"novembre/décembre 2012"}

3.2 Lecture d’un fichierLa lecture d’un fichier JSON ne pourra se faire de manière efficace que si l’on

connaît la structure du fichier de manière à créer un type correspondant à cette structure et à pouvoir l’utiliser par la suite. Comme nous venons d’enregistrer le fichier de données avec une structure Magazine, cela ne pose pas de problème (sinon il suffit d’analyser le fichier pour en extraire la structure) :

01: package main 02: 03: import ( 04: "fmt" 05: "encoding/json" 06: "io/ioutil" 07: "strconv" 08: ) 09: 10: type Magazine struct { 11: Numero int 12: Date_parution string 13: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com64

Pour aLLer PLus LoiN traiteMeNt des fichiers

14: 15: func main() { 16: fic, err := ioutil.ReadFile("data.json") 17: 18: if err != nil { 19: panic(err) 20: } 21: 22: var m Magazine 23: json.Unmarshal(fic, &m) 24: fmt.Println("N." + strconv.Itoa(m.Numero) + " de " + m.Date_parution) 25: }

Dans les lignes 10 à 13, le type Magazine est à nouveau défini pour pouvoir être utilisé lors de la lecture. Dans la « vraie vie », nous l’aurions placé dans un paquetage indépendant de manière à le charger dans la partie écriture ou lec-ture. Après lecture du fichier en ligne 16, nous transformons les données JSON en variable de type Magazine en ligne 23. Cette variable peut ensuite être utilisée tout à fait normalement, en faisant référence aux différents champs (ligne 24).

4 Les fichiers binaires GoLes fichiers binaires Go, d’extension gob, permettent d’enregistrer et de lire

simplement des objets Go. Le paquetage permettant de manipuler de tels fichiers se nomme encoding/gob.

4.1 Écriture dans un fichierLa méthode Encode(), appliquée à un objet de type Encoder, va permettre

de sauvegarder le contenu de variables de différents types. L’utilisation de cet encodage est destinée à un travail sur fichiers, donc il suffit de passer un pointeur sur fichier en paramètre de la fonction d’encodage pour obtenir un enregistre-ment des données. Voici par exemple comment sauvegarder les données (et la structure) d’une variable de type Magazine :

01: package main 02: 03: import ( 04: "encoding/gob" 05: "os" 06: ) 07: 08: type Magazine struct { 09: Numero int 10: Date_parution string 11: } 12: 13: func main() { 14: fic, err := os.OpenFile("data.gob", os.O_CREATE | os.O_WRONLY, 0666) 15: defer fic.Close() 16: 17: if err != nil { 18: panic(err) 19: } 20: 21: m := Magazine{Numero : 62, Date_parution : "novembre/décembre 2012"} 22: 23: encoder := gob.NewEncoder(fic) 24: encoder.Encode(m) 25: }

Comme l’objet Encoder nécessite un pointeur sur fichier, nous ne pouvons plus utiliser le paquetage ioutil pour la gestion des fichiers. Nous utiliserons donc une ouverture par le paquetage os (ligne 14), avec fermeture en sortie de fonction (ligne 15). Nous créons ensuite une variable de type Magazine (ligne 21), puis nous créons l’encodeur sur le fi-chier précédemment ouvert (ligne 23) et enfin, nous stockons les données de la variable m au format binaire dans notre fichier (ligne 24).

4.2 Lecture d’un fichierL’opération de lecture d’un fichier gob

sera complètement symétrique à l’opé-ration d’écriture. Au lieu d’utiliser un encodeur, nous utiliserons un décodeur :

01: package main 02: 03: import ( 04: "encoding/gob" 05: "fmt" 06: "os" 07: "strconv" 08: ) 09: 10: type Magazine struct { 11: Numero int 12: Date_parution string 13: } 14: 15: func main() { 16: fic, err := os.Open("data.gob") 17: defer fic.Close() 18: 19: if err != nil { 20: panic(err) 21: } 22: 23: var m Magazine 24: 25: decoder := gob.NewDecoder(fic) 26: decoder.Decode(&m) 27: fmt.Println("N." + strconv.Itoa(m.Numero) + " de " + m.Date_parution) 28: }

L’ouverture se fait ici en lecture seule (ligne 16) et la variable m ne contient bien sûr pas de données (ligne 23). Le décodeur est créé en le liant au fichier précédemment ouvert (ligne 25) et la méthode Decode() de la ligne 26 permet de lire les données et de les stocker dans m.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 65

Pour aLLer PLus LoiN traiteMeNt des fichiers

5 Compression de donnéesLa bibliothèque standard de Go propose des paquetages permettant de ma-

nipuler différents formats de compression tels que gzip, zip et tar. Nous nous intéresserons au format zip avec le paquetage archive/zip.

5.1 Créer une archive ZIPPour créer une archive, il faut créer un fichier d’extension zip dans lequel nous

stockerons des fichiers. La création d’un objet Writer nous permettra justement d’ajouter des fichiers au fur et à mesure dans notre archive :

01: package main 02: 03: import ( 04: "archive/zip" 05: "os" 06: "io/ioutil" 07: ) 08: 09: func main() { 10: fic, err := os.OpenFile("data.zip", os.O_CREATE | os.O_WRONLY, 0666) 11: defer fic.Close() 12: 13: if err != nil { 14: panic(err) 15: } 16: 17: fileList := []string{"data.json", "data.gob"} 18: 19: archive := zip.NewWriter(fic) 20: defer func () { 21: err := archive.Close() 22: if err != nil { 23: panic(err) 24: } 25: }() 26: 27: for _, filename := range fileList { 28: fic, err := archive.Create(filename) 29: if err != nil { 30: panic(err) 31: } 32: data, err := ioutil.ReadFile(filename) 33: if err != nil { 34: panic(err) 35: } 36: fic.Write(data) 37: } 38: }

L’ouverture du fichier en écriture se fait de manière classique pour obtenir un pointeur sur fichier avec le paquetage os (ligne 10). Nous plaçons la liste des fichiers à intégrer dans l’archive dans un slice fileList en ligne 17. L’objet Writer est créé en ligne 19, grâce à la fonction NewWriter() et sera stocké dans la variable archive. Les lignes 20 à 25 permettent de s’assurer que l’archive est correctement refermée à la fin de l’exécution du programme et qu’aucune erreur pouvant la corrompre ne s’est produite. Nous parcourons ensuite la liste des fi-chiers à archiver (lignes 27 à 37) et pour chacun d’eux, nous l’ajoutons à l’archive par la méthode Create() (ligne 28) et nous copions son contenu dans l’archive.

Cette copie s’effectue en deux temps : lecture des données en ligne 32, puis écriture dans l’archive en ligne 36.

5.2 Lire une archive ZIP

Pour la lecture d’une archive zip, le paquetage archive/zip fournit une fonction OpenReader() qui lit directe-ment le fichier d’archive et renvoie un objet Reader.

01: package main 02: 03: import ( 04: "archive/zip" 05: "fmt" 06: ) 07: 08: func main() { 09: archive, err := zip.OpenReader("data.zip") 10: defer archive.Close() 11: 12: if err != nil { 13: panic(err) 14: } 15: 16: fmt.Println("Liste des fichiers de l’archive\n") 17: for i, fic := range archive.File { 18: fmt.Printf("Fichier n.%d : %s\n", i+1, fic.Name) 19: } 20: }

La lecture de l’archive est effectuée en ligne 9. Nous pouvons ensuite par-courir la liste des fichiers archive.File (lignes 17 à 19) pour afficher le nom des fichiers de l’archive (ligne 18). Si vous souhaitez lire l’un des fichiers fic.Name, il vous suffit d’appliquer l’une des méthodes que nous avons vues au cours de cet article...

ConclusionLes paquetages de Go permettent de

traiter différents formats de fichier de manière très simple. On est bien loin de la gestion du C et le développeur peut, une fois de plus, se consacrer à des tâches plus importantes que l’ouverture et la fermeture d’un fichier !

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com66

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

Les TesTs en Go C'esT TeLLeMenT sIMPLe qUe VoUs DeVrIeZ Les TesTer !

par Jean-Michel Armand

Les décennies passent et ne se ressemblent vraiment pas. Il n’y a pas si longtemps, un code qui fonctionnait était un code qui donnait un résultat. Heureusement pour nous, depuis, le monde a bien changé et les tests sont devenus importants. Dans cet article, nous allons voir ce que le Go propose à ce niveau-là.

1 Petit rappel : les tests, c’est quoi ?

Pour commencer, je vais vous rappeler qu’il y a toutes sortes de tests : les tests unitaires, de charge, de non régression, de performance, les stress tests, les tests de robustesse, les tests fonctionnels ou encore les tests utilisateur. Vous avez peut-être l’impression que la liste que je viens de faire est longue, mais elle est pourtant loin d’être exhaustive ! Il y a donc toutes sortes de tests. Nous verrons dans la suite de cet article que le Go vous propose des outils pour mettre en place les tests unitaires et les tests de performance.

Vous donner une liste de ce qu’il est possible de faire comme test ne fait pas vraiment avancer les choses. Vous devez vous demander pourquoi et comment tester. Revenons quelques années en arrière, ou alors trouvons un développeur actuel qui, honte sur lui, ne code pas de test. Comment savoir si le programme que l’on est en train de développer fonctionne ? On le lance et on le teste en condition « réelle », de la même façon que si on l’utilisait pour de vrai. Et pour être sûr que l’on ne rate rien, on peut même se définir un cahier de recettes, qui liste toutes les étapes que l’on doit faire, exactement comme une check list dans un avion. Mais à chaque fois que l’on fait des modifications sur le projet,

il faut tout refaire, pour tout vérifier, être sûr qu’il n’y a pas d’effet de bord et que le code que l’on vient d’ajouter fonctionne correctement. Tout cela engendre tout d’abord une énorme perte de temps, parce que rien n’est automatisé. Il faut faire et refaire, à intervalles réguliers, tous les tests.

Mais cela peut générer aussi beaucoup d’incertitude. Parce qu’imaginons le pire cas, celui où l’on trouve un bug. Il faut alors trouver d’où il vient, quelle partie du programme plante. Et si l’on imagine que l’on est confronté à un programme d’une taille conséquente, cela peut de-venir encore plus ardu que de retrouver une aiguille dans une meule de foin. On se retrouve en effet à devoir déboguer quasiment à la main, au breackpoint et au printf. Il y a des situations plus enviables que cela. Et pour quelle raison ? Pour la simple raison que rien n’est unitaire. On fait des tests sur un programme dans sa globalité et pas petit bout par petit bout. La solution pour régler le problème ? Ce sont tout bêtement les tests unitaires automatisés.

1.1 Tests unitaires, comment les écrire ?

Écrire des tests unitaires n’est pas for-cément trivial. Il faut savoir décider quoi tester et comment tester. Tout d’abord, il faut bien se rappeler que le mot unitaire n’est pas là pour rien. Vos tests doivent

donc tester des morceaux de code qui sont vraiment unitaires. Cela peut être une classe et toutes ses méthodes, ou alors une fonction ou un ensemble de fonctions dans un module. Mais cela doit se limiter à cela. Et plus important encore, il faut que vous soyez sûr que la portion de code que vous allez tester sera, lors du test, en isolation complète. Le morceau de code que vous allez tester, ne doit être dépendant d’aucun autre bout de code. C’est impossible allez-vous me rétorquer. Après tout, si vous utilisez un channel ou une goroutine ou un Slice, vous êtes dépendant d’autres morceaux de code. C’est vrai. Mais dans ce cas-là, il faut que les bouts de code dont vous êtes dépendant soit eux-mêmes couverts par des tests. Vous pouvez voir les choses comme une pyramide. Votre module ou votre classe, se trouve être un étage intermédiaire de la pyramide des tests. Vous ne pouvez le construire que parce que tout ce qu’utilise votre module a été testé et que ces tests sont l’étage inférieur de votre pyramide de test. Quant à l’étage supérieur, il représentera d’autres tests, ceux qui testeront le fait que deux de vos modules interagissent correctement entre eux. Et cela ainsi de suite, jusque tout en haut de votre pyramide de tests.

Si votre paquetage est difficile à tester, c’est peut-être qu’il est trop gros ou qu’il contient trop de dépendances. L’écriture des tests est un moment

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 67

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

parfait pour faire du refactoring sur son code. Profitez-en ! Après tout, s’il est facile d’écrire des tests, il sera facile d’utiliser votre code à l’intérieur de votre programme. A contrario, si vous avez déjà du mal à écrire des tests, comment pourriez-vous utiliser votre code en situation réelle d’une manière fluide ?

Je disais quelques lignes plus haut que le code que vous testiez ne devait pas être dépendant d’autres morceaux de code (à moins que ceux-ci soit totalement testés). C’est la même chose pour tout ce qui est entrée/sortie, périphérique de stockage ou autre. Vous aurez le temps d’ajouter des tests pour savoir si votre programme dialogue correctement avec d’autres programmes au travers d’une socket ou avec une base de données. Là, pour l’instant, contentez-vous de « mocker » vos interfaces avec le monde extérieur.

Souvent, la tentation est forte de trop tester dans un test. Rappelez-vous : un test doit être unitaire ! Si vous avez deux choses différentes à tester, faites deux tests. Vos tests n’en seront que plus clairs.

Écrivez vos tests le plus rapidement possible après avoir écrit le code que vous devez tester. Vous pouvez même écrire vos tests avant, si vous êtes un adepte du TDD (voir encadré). Mais si vous préférez les écrire après, écrivez-les dès que vous avez fini de développer votre module. Les choses que l’on remet à demain sont celles que l’ont ne fait jamais.

Le Test Driven Development (ou TDD)Le TDD est une technique de développement qui promeut le fait d’écrire le test avant d’écrire le code à tester. Le but de cette technique est double. Tout d’abord, on a plus tendance à concevoir son code en termes de Design for Testability (notion qui provient au départ de la conception de composants électroniques complexes. Elle indique le fait de prendre en compte, dès la conception d’un composant, le coût qu’aura le test complet de celui-ci et la volonté de concevoir l’élément de telle façon que ce coût en sera le plus petit possible). Enfin, on se retrouve à tester du code qui n’est pas encore écrit. Mais qui dit tester, dit l’utiliser. On est donc dans une vraie phase d’utilisation du code (même si celui-ci est encore inexistant). On est donc censé pouvoir plus facilement détecter les problèmes de design d’API et les corriger immédiatement.

Pourquoi est-ce plus facile ? Parce que justement le code n’est pas écrit. Imagi-nons que nous avons une classe très compliquée que nous venons de finir de coder. Nous commençons à écrire des tests. Et en écrivant les tests, on se rend compte que les signatures des méthodes ne vont pas, qu’elles nous limitent et que d’autres signatures auraient été bien plus souples. Logiquement, nous devrions réécrire le code de notre classe, tant pis s’il faut pour cela « jeter » une bonne partie du code. Mais au vu de l’effort déjà fourni, la tentation serait grande de se dire que ce n’est pas si grave, que l’on peut trouver des manières d’utiliser notre classe, même si son API n’est pas parfaite, que l’on pourra s’arranger avec l’existant. Et l’on va introduire des faiblesses dans le design de notre application. Faiblesses qui, petit à petit, en s’ajoutant les unes aux autres, vont finir par dégrader très fortement la qualité de notre application. Alors qu’au contraire, si vous écrivez les tests avant, vous avez simplement imaginé l’API de votre classe, vous ne l’avez pas encore codée pour de vrai. Quand, en mettant en place vos tests, vous vous rendez compte que ce que vous aviez imaginé était plutôt mauvais, cela ne vous coûte rien de rectifier les choses. Et la qualité générale de votre programme s’améliore. Pour finir avec le TDD, voici une brève description du cycle de développement conseillé pour le TDD. Celui-ci se découpe en cinq étapes : 1) Écrire un premier test ;2) Vérifier qu’il échoue (ce qui est normal, le code n’existe pas, mais on vérifie

ainsi que le test ne contient pas lui-même une erreur, ce qui serait le cas s’il n’échouait pas) ;

3) Écrire le code juste suffisant pour que le test passe, même si c’est du code qui vous fait rougir de honte ;

4) Vérifier que le test passe ;5) « Refactorer » votre code pour que vous n’ayez plus honte de celui-ci et qu’il

devienne du code utilisable en production.1.2 Tests unitaires, comment bien les écrire ?

Nous avons vu comment écrire des tests dans le paragraphe précédent. Avant d’en venir dans la prochaine partie à un peu de pratique, voyons une ou deux dernières choses. Tout d’abord, comment bien écrire des tests ? Il y a deux notions importantes pour savoir si un test est bien écrit. La première notion est celle de la couverture de code. Votre code est couvert s’il est testé par au moins un test. Le but est

alors d’atteindre une couverture de 100%. Certains outils d’intégration et de « build » automatisés vous calculent d’ailleurs la couverture de votre code. D’autres outils en ligne de commandes le font également. C’est en effet une notion intéressante... mais qui a ses limites et qu’il ne faut pas voir comme un Graal absolu. En effet, vous pouvez très bien avoir un code couvert à 100%

par des tests et pourtant plein de bugs qui ne sont pas remontés par les tests. Tout simplement parce que vos tests ne seront pas complets. Et c’est là la deuxième notion importante pour écrire de bons tests, la notion de complétude et de tests aux limites. Il ne faut en effet pas seulement tester les cas nominaux, mais bien penser aux cas qui n’arrivent jamais, parce qu’un jour ils finiront par

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com68

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

arriver. Prenons une fonction qui fait des additions d’entiers. Vous avez fait des tests. Bien. Mais avez-vous pensé à tester ce qui se passait lorsque la somme de vos deux chiffres dépassait la capacité stockable par la taille mémoire de votre variable de résultat ? C’est ce genre de test auquel il faut penser et qui fait toute la différence, à couverture de code égale, entre un programme bien testé et un programme mal testé.

1.3 extra bonus point : les tests, ça peut servir à autre chose qu’à tester ?

Tester c’est bien. Ça permet de corriger les bugs rapidement, voire de les éradiquer avant même qu’ils n’apparaissent. Mais les tests peuvent avoir d’autres intérêts que le simple test. Il y a, on l’a déjà vu, la possibilité d’améliorer le design des API de nos classes. Mais il y a aussi un vrai potentiel de documentation dans les tests. Alors effectivement, les tests ne remplacent pas une documentation, ça serait bien trop facile. Mais ils sont une aide fantastique à la compréhension du code. Ils permettront à toute personne qui voudrait comprendre votre code de le comprendre bien plus vite que ce que ne le permettrait la seule lecture de la documentation, parce qu’au final, la documentation reste de la documen-tation, alors que les tests permettent de voir le code s’exécuter, en voir les différents enchaînements.

Enfin, et là cela s’applique plus par-ticulièrement aux librairies, les tests feront que votre code sera utilisé, tout simplement. En effet, au moment de choisir d’utiliser une librairie, un dé-veloppeur va vérifier différents points, comme le fait que la bibliothèque ne soit pas totalement morte, que la doc existe et est à jour, mais aussi qu’il existe des tests. Et si je me fonde sur mon expérience et mes discussions avec d’autres développeurs, ce point devient de plus en plus critique dans le choix d’une bibliothèque. Ce qui fait que si par hasard, vous écriviez la plus belle bibliothèque du monde, sans aucun bug

ni problème, mais que vous négligiez d’écrire des tests, vous auriez presque travaillé pour rien, vu le nombre de gens qui, repoussés par l’absence de tests, refuseraient d’utiliser votre travail.

2 Les tests en GoLe Go vous propose un paquetage

d’aide à la rédaction des tests, le pa-quetage testing. Ce paquetage vous permettra de faire trois types de tests : des tests unitaires classiques, des tests de performance et des tests de vérification de sortie. Avant d’étudier un petit bout de code, quelques petites conventions d’écriture qui sont obligatoires en Go :

- Le nom d’un fichier de test doit se terminer par _test. Le fichier qui est testé et le fichier à tester ne doivent pas obligatoirement avoir le même nom (en omettant le test), le fichier de test de add.go peut parfaitement s’appeler toto_test.go. Mais une bonne pratique recommande que les noms soient semblables ;

- Les fonctions de test doivent avoir leur nom qui commence par Test. Les fonctions dont le nom ne com-mence pas par Test, même si elles se trouvent dans le fichier de test, ne sont pas considérées comme des tests ;

Enfin, pour lancer des tests, il n’y a rien de plus simple : il suffit d’aller dans le répertoire qui contient les paquetages que l’on souhaite tester et lancer la commande go test.

2.1 Les tests unitaires en Go

Voici le fichier que l’on souhaite tester :

01: package addhsgo 02: 03: func Add(a, b int32) (int32) { 04: return a + b 05: } 06: 07: func InvalidAdd(a, b int32) (int32) { 08: return a + b +1 09: }

Nous allons le tester avec le fichier suivant :

01: package addhsgo 02: 03: import ("testing") 04: 05: func TestAdd(t *testing.T) { 06: r := Add(41,1) 07: if r != 42 { 08: t.Errorf("Add de 41 et 1 donne %v au lieu de 42\n",r) 09: } 10: } 11: 12: func TestInvalidAddwithError(t *testing.T) { 13: r := InvalidAdd(41,1) 14: if r != 42 { 15: t.Errorf("InvalidAdd de 41 et 1 donne %v au lieu de 42\n",r) 16: } 17: r = InvalidAdd(32,1) 18: if r != 33 { 19: t.Errorf("InvalidAdd de 32 et 1 donne %v au lieu de 33\n",r) 20: } 21: 22: } 23: 24: func TestAddFail(t *testing.T) { 25: r := Add(41,1) 26: t.Fail() 27: if r != 42 { 28: t.Errorf("InvalidAdd de 41 et 1 donne %v au lieu de 42\n",r) 29: } 30: 31: t.Log("On a lancé un fail mais on est toujours dans la fonction TestAddFail") 32: } 33: 34: func TestAddFailNow(t *testing.T) {35: r := Add(41,1)36: t.FailNow() 37: t.Log("On a lancé un failnow jamais on ne passera ici") 38: if r != 42 { 39: t.Errorf("InvalidAdd de 41 et 1 donne %v au lieu de 42\n",r) 40: } 41: 42: }

On commence donc par importer le paquetage testing ligne 3. Vous pouvez voir que chaque fonction de test a une signature bien précise, elle prend comme argument un t *testing.T. Cet argument est obligatoire. On verra dans les paragraphes suivants que pour les autres types de tests, on utilisera un autre type d’argument. C’est l’argument t qui va nous servir tout au long de nos tests pour faire remonter des erreurs à l’utilisateur. Avant d’aller plus loin dans l’étude du code, regardons ce que donne la sortie de go test :

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 69

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

jmad@Albert:~/Code/Go/Examples/Test2$ go test --- FAIL: TestInvalidAddwithError (0.00 seconds) addhsgo_test.go:16: InvalidAdd de 41 et 1 donne 43 au lieu de 42 addhsgo_test.go:20: InvalidAdd de 32 et 1 donne 34 au lieu de 33 --- FAIL: TestAddFail (0.00 seconds) addhsgo_test.go:32: On a lancé un fail mais on est toujours dans la fonction TestAddFail --- FAIL: TestAddFailNow (0.00 seconds) FAIL exit status 1 FAIL _/home/jmad/Code/Go/Examples/Test2 0.004s

On peut tout d’abord remarquer qu’il n’affiche pas les tests réussis. Concernant les tests qui ont échoué, il nous affiche la fonction de test qui a échoué, la ligne de celle-ci et le message d’erreur.

Retournons au code proprement dit et étudions-le plus attentivement. Plusieurs méthodes de t sont utilisées : vous pouvez voir t.Errorf(), t.Log(), t.Fail() et t.FailNow().

La fonction Fail() marque la fonc-tion de test en cours d’exécution comme ayant raté. Elle n’en arrête toutefois pas l’exécution. La fonction FailNow(), elle, en plus de marquer la fonction comme ayant raté, en arrête immédiatement l’exécution. Vous pouvez d’ailleurs vous en rendre compte dans la trace console de notre go test.

En effet, ma fonction TestAddFail() (définie ligne 24) utilise un Fail() ligne 26, le Log de la ligne 31 est donc bien exécuté. Par contre, la fonction TestAddFailNow (définie ligne 34) utilise un FailNow() à la ligne 36. L’ap-pel à Log() de la ligne 37 n’aura donc jamais lieu, comme toutes les instructions suivantes. Deviner ce que fait la fonction Log() est du coup trivial, elle affiche quelque chose sur la sortie standard. Quant à Errorf() elle se contente de faire un Logf(), suivi d’un FailNow(). Vous avez sûrement remarqué que cer-taines fonctions se finissent par un f et pas d’autres. Typiquement, Errorf() ou Logf(). En fait, chaque fonction du paquetage testing permettant d’affi-cher un message est proposée en deux saveurs : celle dont le nom se termine par un f et celle sans f. L’explication se trouve dans l’encadré ci-contre.

errorf() vs error()La différence entre les fonctions en f et celles ne se terminant pas par f est la même que celle qui existe entre Println() et Print(). En fait, les fonctions en f tout comme Print() prennent en premier argument une chaîne de format. Les arguments suivants sont alors les valeurs qui seront substituées aux caractères de format dans la chaîne de format. Pour ce qui est des fonctions ne ter-minant pas par f et de Println(), elles se contentent d’afficher les ar-guments qu’on leur a passés, les uns après les autres, ajoutant un espace entre chaque argument si nécessaire.

En plus des fonctions que j’ai utilisées dans mon exemple, vous pourrez trouver f.Fatal(), qui peut se modéliser par un t.Log(), immédiatement suivi d’un t.FailNow(). Vous l’avez sûrement re-marqué, je n’ai utilisé aucune assertion dans mon test. C’est tout simplement parce que le Go n’en propose pas. La justification de cela est donné dans une question de la FAQ [1]. En résumé, ils trouvent qu’une gestion des retours et donc des erreurs à la main est plus claire pour le développeur. Que cela limite la longueur des « stacktraces » à analyser et que cela force à avoir des mécanismes précis qui aideront à la compréhension du code. C’est un point de vue. Toutefois, tout le monde n’est pas du même avis que les concepteurs du Go. Il y a donc plusieurs bibliothèques qui proposent des assertions.

2.1.1 assertions en GoOn va pour cela utiliser une petite

bibliothèque, assert [2], disponible sur GitHub. Pour l’installer, il faudra utiliser go get, comme dans la vue console suivante :

jmad@Albert:~/Code/Go/Examples/Test3$ go get github.com/bmizerany/assert

Une fois que cela est fait, on peut réécrire nos tests d’une façon beaucoup plus concise. Pour ne pas rallonger inutilement les morceaux de code présents

dans l’article, je n’ai réécrit que les deux premières fonctions de test :

01: package addhsgo 02: 03: import ( 04: "testing" 05: "github.com/bmizerany/assert" 06: ) 07: 08: func TestAdd(t *testing.T) { 09: r := Add(41,1) 10: assert.Equal(t, r, int32(42)) 11: 12: } 13: 14: func TestInvalidAddwithError(t *testing.T) { 15: r := InvalidAdd(41,1) 16: assert.Equal(t, r, int32(42)) 17: }

Vous remarquerez que les fonctions de test ont fondu au niveau du nombre de lignes. Forcément, à la place d’un test sur le retour et d’un appel manuel à une fonction t.Error(), il n’y a plus ici qu’un appel à une assertion. Vous remarquerez également que je suis du coup obligé de convertir explicitement mes valeurs de retour à cause du typage fort. Si je ne le fais pas, le compilateur m’indiquera avec une jolie erreur qu’il ne peut comparer int32 et int. Concer-nant le retour console, il est similaire au premier. Voyez par vous-même :

jmad@Albert:~/Code/Go/Examples/Test3$ go test --- FAIL: TestInvalidAddwithError (0.00 seconds) assert.go:15: /home/jmad/Code/Go/Examples/Test3/addhsgo_test.go:16 assert.go:24: ! 43 != 42 FAIL exit status 1 FAIL _/home/jmad/Code/Go/Examples/Test3 0.006s

Par contre, les messages ne sont plus des messages que j’ai moi-même définis, je n’ai donc plus que des mes-sages générés, potentiellement un peu moins clairs. Si vous regardez en détail la bibliothèque assert, vous verrez qu’elle ne propose pour l’instant que deux assertions Equal() et NotEqual(). C’est peu, il est vrai, mais ce sont aussi les deux assertions les plus utilisées. Si vous souhaitez en avoir plus et que vous êtes d’humeur contributive, je vous encourage à forker le dépôt et à essayer de coder celles qu’il vous manque. Si vous n’êtes pas d’humeur à contribuer, il existe d’autres bibliothèques plus complètes pour mettre en place des assertions, comme

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com70

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

gocheck [3]. Son fonctionnement est toutefois un peu plus compliqué que la librairie que nous avons vue ci-dessus.

2.2 Tests de performanceEn plus de vous proposer des tests

unitaires, le Go vous propose des fonctionnalités de benchmarking, cela afin de tester les performances de votre code. Vos fonctions de test de performance devront toujours se trouver dans un fichier dont le nom finit par _test. Par contre, leur nom ne devra pas commencer par Test, mais par Benchmark et à la place d’avoir des fonctions qui prennent en argument un t *testing.T, elles vont prendre en argument un b *testing.B. Si nous voulons tester les performances de notre fonction Add() définie précédemment, nous allons alors avoir le fichier suivant :

01: package addhsgo 02: 03: import "testing" 04: 05: func BenchmarkAdd(b *testing.B) { 06: for i := 0; i < b.N; i++ { 07: Add(42,2) 08: } 09: }

Ligne 5, on trouve bien la fonction de Benchmark avec son argument b. Ensuite, on définit une boucle qui va itérer jusqu’à atteindre b.N (ligne 06). En fait, b.N signifie « jusqu’à ce que le temps mesuré veuille dire quelque chose ». Lorsque l’on lance les tests, on obtient alors le résultat suivant :

jmad@Albert:~/Code/Go/Examples/Bench$ go test -test.bench="Benchmark*" testing: warning: no tests to run PASS BenchmarkAdd 2000000000 0.93 ns/op ok _/home/jmad/Code/Go/Examples/Bench 1.960s

Avant de détailler le résultat, regardez la façon de lancer la commande. Pour que les benchmarks se lancent, il faut passer à go test le paramètre -test.bench. Celui-ci prend en argument une expression régulière. Seules les fonctions de benchmark dont le nom valide cette expression régulière seront exécutées. Ensuite, vous voyez que Go vous signale qu’il n’y a pas de test à lancer... C’est normal, nous ne faisons que des benchmarks.

Enfin, nous arrivons à la partie qui nous intéresse : le lancement des fonc-tions de test de performance. Chaque ligne est constituée de trois colonnes ; la première correspond au nom de la fonction, la deuxième (ici 2000000000) correspond au nombre de fois que la boucle a dû être exécutée (vous vous souvenez du b.N) pour que le résultat ait un sens. Enfin, la troisième vous indique combien de temps a pris une itération de boucle. Cette méthode a toutefois un problème. En effet, on ne peut contrôler le démarrage du timer qui va servir au benchmark, celui-ci étant lancé dès que la fonction de test de performance s’exé-cute. Il y a toutefois moyen de contrôler celui-ci, avec deux méthodes des objets *testing.B : les méthodes StopTimer() et StartTimer(). Imaginons que notre test de performance de la fonction d’addition demande une très lourde initialisation, notre code deviendrait alors :

01: package addhsgo 02: 03: import "testing" 04: 05: 06: func BenchmarkAdd(b *testing.B) { 07: b.StopTimer() 08: //Initialisation très longue 09: b.StartTimer() 10: for i := 0; i < b.N; i++ { 11: Add(42,2) 12: } 13: }

Une dernière chose concernant les fonctions de benchmark : les objets *testing.B, en plus d’être capables de vous donner les temps d’exécution, savent aussi se comporter comme des objets *testing.T. C’est-à-dire que vous pouvez vous en servir pour lancer des Fatal(), Fail(), FailNow(), Log(), Error() et autres fonctions de test.

2.3 Tests de sortieIl existe un troisième type de test

que l’on peut faire en Go, les tests de sortie, qui sont appelés en Go « tests d’exemple ». Le principe de ces tests est simple : vous mettez en place des fonctions de test dont le nom doit commencer par Example. Ces fonctions sont censées renvoyer des choses sur la sortie standard. Vous allez indiquer

directement dans le corps des fonctions quels sont les retours attendus. Le Go vérifiera ensuite que c’est bien le cas.

01: package main 02: 03: import "fmt" 04: 05: func ExampleGadget() { 06: fmt.Println("Bonjour, je suis l’inspecteur Gadget") 07: // Output: Bonjour, je suis l’inspecteur Gadget 08: } 09:10: func ExampleGadgetetFino() { 11: fmt.Println("Bonjour, je suis l’inspecteur Gadget") 12: fmt.Println("Voici Sophie ma nièce et son chien Fino") 13: // Output: 14: // Bonjour, je suis l’inspecteur Gadget 15: // Voici Sophie ma nièce et son chien Fino 16: } 17: 18: func ExampleGadgetErreur() { 19: fmt.Println("Go Go Gadgeto Main") 20: // Output: Go Go Gadegto Laser 21: }

Comme vous pouvez le voir, les trois fonctions de test contiennent directement, après la balise Output, les sorties que l’on s’attend à avoir. Vous remarquerez également que l’import de testing n’est ici pas nécessaire. La sortie console du go test lancé sur ce fichier donne le résultat suivant :

jmad@Albert:~/Code/Go/Examples/test4$ go test --- FAIL: ExampleGadgetErreur (0.00 seconds) got: Go Go Gadegto Main want: Go Go Gadegto Laser FAIL exit status 1 FAIL _/home/jmad/Code/Go/Examples/test4 0.004s

Comme à son habitude, go test n’affiche que les tests en erreur. Il affiche donc ici ce qu’il s’attendait à avoir et ce qu’il a eu au final.

3 et si on ajoutait un peu d’intégration continue ?

Jusqu’à présent, on a beaucoup parlé de tests et d’écriture de tests, mais il reste une étape qui est manuelle, le fait de lancer des tests. Il faut en effet penser à lancer votre commande go test de

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 71

Pour aLLer PLus LoiN Les tests eN Go c'est teLLeMeNt siMPLe que Vous deVriez Les tester !

manière régulière pour bien vérifier que vous n’avez pas ajouté de bug. C’est ici qu’interviennent les outils d’intégration continue. Ce sont des outils qui vont, à intervalles réguliers, récupérer votre code source sur votre gestionnaire de sources, lancer la totalité de vos tests, puis publier un rapport qui vous en donnera le résultat. Nous allons voir comment cela peut fonctionner en Go. Parmi tous les outils qui existent, j’ai décidé de vous présenter Travis CI [4]. Pourquoi choisir Travis CI ? Parce que c’est un système d’intégration en licence libre que vous pouvez donc forker sur GitHub [5], mais aussi parce que l’intégration avec GitHub est vraiment très simple et que si vous avez des projets GitHub publics, vous allez pouvoir utiliser la plateforme Travis CI sans avoir à déployer la vôtre.

J’ai donc créé un dépôt GitHub [6] qui contient uniquement les deux fichiers du paquetage addhs. Vous pourrez le récupérer en lançant la commande git clone https://github.com/ mrjmad/hs_linuxmag_go.git. Une fois cela fait, je suis simplement allé sur le site de Travis CI, je m’y suis loggué avec mon compte GitHub et j’ai activé le support de Travis CI pour le projet hs_linuxmag_go. Mais là, rien ne se passe. C’est normal, Travis ne lance vos tests que lorsque vous faites des push. De plus, vous n’avez pas encore configuré votre dépôt pour que Travis CI puisse fonctionner. Nous allons faire les deux choses en même temps. Nous allons mettre en place la

configuration de Travis CI et « pusher » celle-ci pour que Travis commence à lancer les tests pour nous. Pour la configuration de Travis CI, il suffit d’ajouter un fichier .travis.yml à la racine de votre dépôt. Le fichier .travis.yml est un simple fichier texte en syntaxe YAML. Le fichier le plus simple possible pour du Go est celui-ci :

01: language: go

Pour notre test, j’ai ajouté une petite ligne pour désactiver les notifications que Travis vous envoie par mail. On obtient alors le fichier suivant :

01: language: go 02: notifications: 03: email: false 04:

Comme je vous le disais, rien de plus simple. Une fois ce fichier « pushé » sur votre dépôt, il vous suffit d’attendre que Travis CI vous alloue un worker et lance vos tests. Vous n’aurez plus alors qu’à aller sur la page Travis CI de votre projet (pour le projet de test, c’est celle-ci : https://travis-ci.org/#!/mrjmad/hs_linuxmag_go) et vous verrez le résultat de vos builds. Vous remar-querez que cette page est publique. En effet, pour les projets publics GitHub, les résultats des builds sont aussi publics. Comme je le disais au tout début de ce paragraphe, la mise en place de Travis CI est très, très rapide. En moins de cinq minutes, si votre projet est déjà sur GitHub, vous avez activé un processus

Fig. 1 : Capture d’écran de l’interface de Travis CI

ConclusionLe langage Go est un langage plutôt

bien fourni pour tout ce qui concerne les tests. La commande go test possède d’ailleurs d’autres paramètres assez inté-ressants (que l’on pourra trouver avec un go help test, un go help testfunc ou un go help testflag). Nous avons toutefois vu les principaux et ils vous fournissent de nombreux outils pour mettre en place une politique de tests automatisés. Il est vrai que l’absence d’assertion complique un peu les choses, mais il n’y a rien d’insurmontable. Vous n’avez donc plus aucune excuse pour ne pas écrire des tests, des tas de tests. J’espère qu’au-delà de l’aspect pratique de l’écriture de tests, la première partie de l’article vous aura convaincu qu’il faut écrire des tests, que cela soit pour votre tranquillité d’esprit de développeur, comme pour celle des gens qui utilise-ront vos programmes ou votre code. Et souvenez-vous bien qu’une bonne ligne de code, c’est une ligne pour laquelle il y a au moins trois lignes de tests !

Références[1] Pourquoi le Go ne propose pas d’assertion :

http://golang.org/doc/go_faq.html#assertions[2] Dépôt assert de bmizerany sur GitHub :

https://github.com/bmizerany/assert[3] gocheck : http://labix.org/gocheck[4] Travis CI : https://travis-ci.org/[5] Dépôt GitHub de Travis CI : https://github.com/travis-ci[6] Dépôt pour le test Travis CI :

https://github.com/mrjmad/hs_linuxmag_go[7] Travis CI avec Bitbucket : http://pythonwise.blogspot.

fr/2012/05/using-travis-ci-with-bitbucket.html

complet d’intégration continue. Et nous n’avons fait qu’effleurer la configuration possible de Travis CI. Vous pouvez par exemple tout à fait lui faire gérer l’ins-tallation des dépendances nécessaires au lancement de vos tests. Enfin, une nouvelle qui réjouira ceux qui utilisent Bitbucket, il est possible d’utiliser Travis avec celui-ci [7].

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com72

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

Marre De La roUTIne ? PasseZ à La ConCUrrenCe aVeC Les GoroUTInes !

par Jean-Michel Armand

La plupart des langages vous proposent leur mécanisme de mise en place des traitements concurrents. Thread, fork, coroutines sont des moyens possibles pour faire cela. Dans ce domaine, le Go innove et vous propose une nouvelle façon de mettre en place la concurrence : les goroutines.

1 PrésentationMais que sont les goroutines ? Ce sont des closures (en

français, on parlera de fermetures ou de clôtures), qui sont exécutées de manière concurrente dans le même espace mémoire. D’un point de vue de la stricte implémentation, ce sont des fonctions qui sont exécutées chacune dans un thread de votre OS.

Les fermeturesUne fermeture est créée à chaque fois qu’une fonc-tion capture des références vers des variables libres, des variables qui ne sont pas des variables locales à cette fonction, sans être des variables globales. Une fermeture est donc créée lorsqu’une fonction f1 est définie dans le corps d’une autre fonction f et que ladite fonction f1 utilise des variables qui sont des arguments ou des variables locales de f. Le concept des fermetures est un vieux concept qui a été théorisé en 1960 et implémenté pour la première fois en 1975 dans le langage Scheme. Historiquement, l’utilisation des fermetures est plus liée aux langages fonctionnels, les langages impératifs ne supportant que très peu, voire pas du tout celles-ci.

Mais plus important que ce qu’elles sont, c’est la façon de les utiliser qui est importante. Le Go propose une nouvelle vision de la gestion de la concurrence dans les programmes. Le langage part du principe que gérer le partage de mémoire est une chose compliquée et bien trop source de bugs. Il propose donc de renverser les choses

et non pas de partager les données, mais de les envoyer de goroutine en goroutine. Cette approche a été synthétisée en un slogan :

« Do not communicate by sharing memory; instead, share memory by communicating. »

Pour permettre cette communication, le Go propose un mécanisme intéressant, les channels, qui ressemblent très fortement à des pipes Unix. Pour communiquer, les goroutines n’ont qu’à s’envoyer des informations à travers différents channels, tout comme peuvent le faire différents processus sous Unix grâce aux pipes. Bien entendu, cette philosophie poussée trop loin peut être contre-productive. Il sera parfois bien plus intéressant d’utiliser un mutex si, par exemple, on veut simplement utiliser un compteur partagé. Mais dans de nombreux cas, lorsque l’on traite une problématique complexe, ce mécanisme de channels permet de simplifier les choses, en tout cas c’est ce que pensent les concepteurs du Go. Cela sera à vous de juger par la pratique.

2 Go go gadgeto-routine !Cette petite présentation faite, passons au code et voyons

comment se présentent les goroutines.

01: package main 02: 03: import ("fmt" 04: "time") 05: 06: func InspectorGadget(message string, delay time.Duration) { 07: go func() {08: time.Sleep(delay)09: fmt.Println(message)

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 73

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

10: }() 11: } 12: 13: func main() { 14: InspectorGadget("Go Go Gadgeto-main", 2*time.Second) 15: InspectorGadget("Go Go Gadgeto-cravate", 3*time.Second) 16: time.Sleep(6*time.Second) 17: }

Les durées en Go Nous aurons l’occasion d’étudier plus en détail le paquetage time [1] dans les paragraphes qui suivent, mais ce premier exemple de code est l’occasion de parler de la manière dont le Go traite les durées. Il utilise le type time.Duration pour cela. C’est en fait un simple renommage de int64. L’unité utilisée pour les Duration est la nanoseconde. Mais nous ne comptons pas tous couramment en nanosecondes. Le pa-quetage time met à notre disposition différentes constantes pour nous simplifier les choses (time.Second, time.Minute ou time.Hour). Au final, si vous voulez indiquer une durée de dix secondes vous devrez donc faire 10*time.Second.

La plupart des lignes de ce petit programme doivent vous être assez familières (après avoir lu l’encadré sur time). Pourtant, dans ces quelques lignes se cache la définition de la goroutine et deux appels à celle-ci. Toute la magie a lieu entre les lignes 6 et 10. On définit en effet en ligne 7 une fermeture func() qui se contente de faire deux choses : une attente en ligne 8 et un affichage de message en ligne 9. Cette fermeture est transformée en goroutine grâce à l’ajout du mot-clé go.

Attention : l’appel de la fermeture en ligne 10, fait grâce au (), est très important ! Sans celui-ci, cela ne fonctionne pas. Et vous pouvez le constater, notre fermeture a bien accès aux variables message et delay qui sont pourtant des arguments de la fonction InspectorGadget.

Le résultat du lancement de ce petit bout de code est le suivant :

jmad@Albert:~/Code/Go/Examples$ goplay gorout1.go Go Go Gadget au Main Go Go Gadget au Cravate

Mais vous pouvez vouloir lancer une goroutine directement, en lui passant une fonction existante. Devoir écrire une ferme-ture pour cela ne serait pas élégant du tout. Heureusement le Go prévoit ce cas de figure et vous pouvez parfaitement faire comme dans le code suivant :

01: package main 02: 03: import ("fmt" 04: "time" 05: "sort") 06:07: func main() {08: gadgets := sort.StringSlice{"Main", "Parapluie", "Laser", "Copter"}

09: fmt.Println(gadgets) 10: go sort.Sort(gadgets) 11: time.Sleep(2*time.Second) 12: fmt.Println(gadgets) 13: }

Ici, l’utilisation d’une goroutine se fait à la ligne 10, juste en ajoutant le mot-clé go devant l’appel de fonction sort.Sort(). Le time.Sleep() de la ligne 11 est simplement là pour forcer le programme principal à attendre la fin du sort.Sort(), histoire que si vous testez ce petit code d’exemple, vous ayez le bon résultat dans votre console.

Le paquetage sortLe paquetage sort [2] offre quelques fonctions et in-terfaces de recherche et de tri. En plus de cela, il définit pour vous différents types de slices, qui ajoutent les fonctions de recherche et de tri aux slices classiques. C’est un de ces types que j’utilise dans le deuxième morceau de code, ligne 08. En plus du StringSlice, sort définit un Float64Slice et un IntSlice.

Ces deux premiers exemples de code montrent combien il est simple de mettre en place des goroutines. Mais pour l’instant, pour être tout à fait honnête, nous n’avons pas grand-chose à nous mettre sous la dent. Nos goroutines tournent chacune dans leur coin et pour être sûrs que le programme ne se termine pas avant la fin de toutes les goroutines, nous sommes obligés de parsemer notre code de time.Sleep(). Ce n’est pas vraiment la panacée. Nous allons donc maintenant voir comment faire parler entre elles nos goroutines.

3 Les channelsJe l’ai dit plus haut, les channels sont des mécanismes

de communication très similaires aux pipes Unix. Voyons tout de suite un exemple pour mieux comprendre comment ils fonctionnent.

01: package main 02: 03: import "fmt" 04: import "time" 05: 06: func InspectorGadget() { 07: seconds := 10 08: time.Sleep(time.Duration(seconds)*time.Second) 09: } 10: 11: func main() { 12: channel1 := make(chan int) 13: fmt.Println("Inspecteur gadget, arrêtez les sbires du Dr. MAD !") 14: go func() { 15: InspectorGadget() 16: channel1 <- 1 17: }() 18: <- channel1 19: fmt.Println("Les méchants sont sous les verrous !!") 20: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com74

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

Ce petit programme, une fois lancé, donnera ce résultat bien prévisible :

jmad@Albert:~/Code/Go/Examples$ goplay gorout3.go Inspecteur gadget, arrêtez les sbires de Dr.MAD ! Les méchants sont sous les verrous !!

Voyons en détail comment il fonctionne. Tout d’abord, ligne 12, je crée notre tout premier channel. Vous remar-querez que je n’utilise pas new, mais make (comme pour les maps). Je définis également que le channel servira à faire passer des int d’une goroutine à une autre. Ensuite, lignes 14 à 17, je définis et je lance ma goroutine. Ici, je n’avais pas envie de passer par une fonction englobante, vu que je n’ai aucun paramètre, je définis donc directement ma fermeture dans la fonction main() et j’ajoute le mot-clé go qui la transforme en goroutine.

Mais que veut dire la ligne 16 ? En fait c’est simple, j’en-voie la valeur 1 (qui est un int) dans le channel channel1. Et à la ligne 18, j’attends que quelque chose arrive du channel1. Ici, je voulais simplement faire en sorte que le main de mon programme attende la fin de la goroutine, sans que j’aie besoin d’utiliser un time.Sleep() ; la valeur que j’injecte d’un côté du channel et que j’attends de l’autre n’a donc aucune importance.

Mais ce n’est pas tout, nous avons pour l’instant créé un channel sans fin, qui peut contenir une infinité de données si celles-ci ne sont pas récupérées à l’autre bout. On peut également définir des channels ayant une taille limite. Il suffit de donner la taille maximum du channel en deuxième argument lorsque l’on appelle make. On obtient alors la syntaxe suivante cb := make(chan int, 50). Si on passe 0 comme taille, alors le channel sera de taille infinie.

Mémo channelLes channels se créent avec make et on doit définir le type des informations qui vont y transiter.Pour injecter une donnée dans un channel c, on utilise la syntaxe c <- Donnée.Pour récupérer une donnée qui arrive d’un channel c, on utilise la syntaxe Donnée <- c.Attendre une donnée d’un channel est une opération bloquante, on peut donc utiliser les channels comme mécanismes de point de rendez-vous entre goroutines.On peut définir un channel ayant une taille maximale en passant la taille en question comme deuxième pa-ramètre à make.

L’utilisation de channel de taille limitée permet de modéliser des sémaphores d’une façon à la fois simple et consistante, avec le paradigme de base de la concurrence en Go. Le morceau de code suivant donne un exemple de cette utilisation des channels :

01: package main 02: 03: import ("fmt" 04: "time" 05: "math/big" 06: "crypto/rand") 07: 08: 09: var entrepot_bp = make(chan int, 2) 10: 11: func MadSbire(boules_puantes chan int) { 12: fmt.Println("je suis un sbire de Dr.Mad et j’attends une boule puante ") 13: fmt.Println("je suis un sbire de Dr.Mad et je vais poser une boule puante") 14: c := big.NewInt(10) 15: delay, _ := rand.Int(rand.Reader, c) 16: fmt.Println("Poser la boule puante me prendra : ",delay.Int64(), " secondes") 17: time.Sleep(time.Duration(delay.Int64()) * time.Second) 18: fmt.Println("je suis un sbire de Dr.Mad et j’ai posé une boule puante") 19: <-boules_puantes 20: } 21: 22: func main() { 23: for { 24: entrepot_bp <- 1 25: go MadSbire(entrepot_bp) 26: } 27: 28: }

Une fois lancé, on obtient le résultat suivant :

jmad@Albert:~/Code/Go/Examples$ goplay gorout4.go je suis un sbire de Dr.Mad et j’attends une boule puante je suis un sbire de Dr.Mad et j’attends une boule puante je suis un sbire de Dr.Mad et je vais poser une boule puante je suis un sbire de Dr.Mad et je vais poser une boule puante Poser la boule puante me prendra : 7 secondes Poser la boule puante me prendra : 6 secondes je suis un sbire de Dr.Mad et j’ai posé une boule puante je suis un sbire de Dr.Mad et j’attends une boule puante

On voit bien ici que le lorsque le channel est plein (ce qui représente ici le fait que tous les sbires sont en train de poser une boule puante), il n’y a plus de nouveau sbire de créé. Dès qu’un sbire a fini de poser sa boule puante, une nouvelle go-routine MadSbire() est lancée et une nouvelle boule puante peut être posée par un des sbires.

Ce morceau de code est l’occasion de découvrir deux nou-veaux paquetages : math/big [3], qui offre une implémentation des très grands chiffres et crypto/rand [4], qui vous permet d’avoir un générateur de nombres aléatoires. Ce paquetage fournit également une fonction Prime(), qui vous renverra un nombre qui a de fortes chances d’être premier.

La ligne 17 est une excellente démonstration du typage fort de Go. En effet, time.Duration est, on l’a déjà dit, un simple renommage de Int64 (type Duration int64) et pourtant, si j’avais écrit la ligne 17 de la façon suivante : time.Sleep(delay.Int64() * time.Second), voilà ce que m’aurait répondu le compilateur :

jmad@Albert:~/Code/Go/Examples$ goplay gorout4.go ERROR: [/home/jmad/Code/Go/go/pkg/tool/linux_386/8g -o /home/jmad/Code/Go/go/pkg/linux_386/.goplay/2197555617/_go_.8 gorout4.go] gorout4.go:17: invalid operation: delay.Int64() * time.Second (mismatched types int64 and time.Duration)

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 75

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

Il faut donc bien convertir le Int64 en time.Duration de manière explicite, même si les deux types semblent être totalement semblables.

Mais les channels ne sont pas là que pour mettre en place des points de rendez-vous ou émuler des sémaphores. Leur véritable raison d’être est bien de permettre à des goroutines de communiquer entre elles. L’exemple suivant montre une telle utilisation des channels :

01: package main 02: 03: import ("fmt" 04: "crypto/rand") 05: 06: func IsPrime(number int64, c chan int64) { 07: if number % 2 ==0 { 08: c <- 0 09: return 10: } 11: var i int64 = 3 12: for ; i < number/2 ; i += 2 { 13: if number % i == 0 { 14: c <- 0 15: return 16: } 17: } 18: c <- number 19: } 20: 21: func main() { 22: r, _ := rand.Prime(rand.Reader,11) 23: fmt.Println("Nombre peut-être premier ", r.Int64()) 24: c := make(chan int64) 25: go IsPrime(r.Int64(), c) 26: s := <- c 27: if s > 0 { 28: fmt.Println("Nombre vraiment premier ", s)29: } 30: }

Une fois exécuté, le code donne la sortie suivante :

jmad@Albert:~/Code/Go/Examples$ goplay gorout5.go Nombre peut-être premier 1787 Nombre vraiment premier 1787

Mais que fait-il vraiment ? Tout d’abord, une petite précision : la fonction rand.Prime() (ligne 22) renvoie un nombre qui est presque certainement premier (la documentation parle d’un nombre qui est « prime with high probability »). Lignes 6 à 18, la fonction IsPrime() est définie. Elle prend en ar-guments deux paramètres : le nombre dont on doit vérifier le caractère premier, et le channel qui nous permettra de renvoyer le résultat. Dans le cas où le chiffre est premier, on le renvoie dans le channel, sinon on envoie un 0. Ligne 26, le corps du programme attend ce que IsPrime() lui renvoie et affichera, ou pas, le fait que le nombre est premier.

Notre exemple souffre toutefois d’une faiblesse : on n’est pas capable de dire si un nombre n’est pas premier. En effet, notre channel de int64 ne permet pas d’envoyer suffisamment d’informations pour cela. Pour mettre en place cela, nous allons voir deux manières. La première, mise en place dans

le prochain bout de code, est toute simple : on définit son propre type (comme vous avez vu qu’il était possible de le faire en Go dans un article précédent) et on définit un channel utilisant ce type précis :

01: package main 02: 03: import ("fmt" 04: "crypto/rand") 05: 06: type prime_number struct { 07: number int64 08: is_prime bool 09: } 10: 11: func IsPrime(number int64, c chan prime_number ) { 12: fmt.Println("Nombre peut-être premier ", number) 13: if number % 2 ==0 { 14: c <- prime_number{number,false} 15: return 16: } 17: var i int64 = 3 18: for ; i < number/2 ; i += 2 { 19: if number % i == 0 { 20: c <- prime_number{number,false} 21: return 22: } 23: } 24: c <- prime_number{number,true} 25: } 26: 27: func main() { 28: c := make(chan prime_number) 29: r, _ := rand.Prime(rand.Reader,11) 30: go IsPrime(r.Int64(), c) 31: go IsPrime(4, c) 32: for i := 0 ; i < 2 ; i +=1 { 33: result := <- c 34: if result.is_prime { 35: fmt.Println("Nombre vraiment premier ", result.number) 36: } else { 37: fmt.Println("Nombre pas premier ", result.number) 38: } 39: } 40: }

On définit donc notre type dans les lignes 6 à 8. Ensuite, on définit notre channel de données de ce type-là en ligne 28. Pour le reste, quasiment rien ne change. Lignes 14, 20 et 24, on ne renvoie simplement plus des int64, mais des instances de notre structure prime_number.

Cette solution est élégante, mais il y a une autre solution : on aurait pu décider de mettre en place deux channels, un pour les nombres premiers et un autre pour les autres. On se retrouverait alors avec un problème. Il faudrait trouver un moyen pour que notre fonction main() attende sur deux channels à la fois. Le Go propose bien entendu une primitive pour cela, primitive qui a d’ailleurs le même nom que celle que l’on utilise en C pour attendre un événement sur plusieurs descripteurs en même temps : select.

01: package main 02: 03: import ("fmt" 04: "crypto/rand") 05:

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com76

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

06: func IsPrime(number int64, prime_chan, not_prime_chan chan int64 ) { 07: fmt.Println("Nombre peut-être premier ", number) 08: if number % 2 ==0 { 09: not_prime_chan <- number 10: return 11: } 12: var i int64 = 3 13: for ; i < number/2 ; i += 2 { 14: if number % i == 0 { 15: not_prime_chan <- number 16: return 17: } 18: } 19: prime_chan <- number 20: } 21: 22: func main() { 23: prime_chan := make(chan int64) 24: not_prime_chan := make(chan int64) 25: r, _ := rand.Prime(rand.Reader,11) 26: go IsPrime(r.Int64(), prime_chan, not_prime_chan) 27: go IsPrime(4, prime_chan, not_prime_chan) 28: for i := 0 ; i < 2 ; i +=1 { 29: select { 30: case number := <- prime_chan : 31: fmt.Println("Nombre vraiment premier ", number) 32: case number := <- not_prime_chan : 33: fmt.Println("Nombre pas premier ", number) 34: } 35: }

Vous remarquerez en ligne 6 l’utilisation de la syntaxe simplifiée de déclaration de plusieurs variables du même type. L’utilisation du select est faite lignes 29 à 33. Comme vous pouvez le voir dans l’exemple, son utilisation est très simple. Pour chaque événement qui peut arriver, on met en place une balise case. Ici, on n’utilise que des événements de lecture, mais on pourrait très bien mixer cela avec un événement d’écriture (case chan <- valeur).

Deux précisions importantes : tout d’abord, si lors d’une attente sur un select plusieurs événements correspon-dant donc à plusieurs case apparaissent en même temps, select en choisira un de manière aléatoire et lancera le case de cet événement et uniquement de celui-là. Ensuite, il existe une possibilité de rendre le select non bloquant, en utilisant le mot-clé default. default sera exécuté si aucune des autres balises case ne s’active. Nous verrons dans quelques lignes un exemple d’utilisation de ce mot-clé default.

3.1 Channels du paquetage timeLe paquetage de gestion du temps time propose dif-

férentes fonctions qui vous permettront de contrôler les goroutines en fonction du temps. Vous allez pouvoir définir des générations d’événements à travers des channels, de manière cyclique ou après une durée bien précise. De même, vous pourrez définir des timers qui « sonneront » après une durée déterminée.

01: package main 02: 03: import ( 04: "fmt" 05: "time" 06: ) 07: 08: func main() { 09: tick := time.Tick(1*time.Second) 10: boom := time.After(5*time.Second) 11: for { 12: select { 13: case <-tick: 14: fmt.Println("Dans quelques secondes, ce message s’auto-détruira") 15: case <-boom: 16: fmt.Println("BOOM!") 17: return 18: default: 19: fmt.Println(".....") 20: time.Sleep(1*time.Second) 21: } 22: } 23: }

Revenons-en à notre cher inspecteur Gadget, qui récupère ses missions sur des messages qui s’auto-détruisent cinq secondes après avoir été lus. Le programme ci-dessus simule cela. Une fois lancé, la sortie du programme est la suivante :

jmad@Albert:~/Code/Go/Examples$ goplay gorout6.go ..... Dans quelques secondes, ce message s’auto-détruira ..... Dans quelques secondes, ce message s’auto-détruira ..... Dans quelques secondes, ce message s’auto-détruira ..... Dans quelques secondes, ce message s’auto-détruira ..... BOOM!

Nous créons ligne 9 un channel tick avec la fonction Tick(d Duration) ; cette fonction va envoyer tous les d temps le temps courant dans le channel renvoyé par Tick (ici, le channel tick). Ensuite, à la ligne 10, nous créons un nouveau channel boom avec la fonction After(d Duration). La fonction After, lorsque la durée d sera passée, enverra le temps courant dans le channel. Ensuite, lignes 12 à 21, on utilise un select pour permettre d’attendre plusieurs évé-nements à la fois. Vous voyez d’ailleurs, ligne 18, l’utilisation de la balise default, qui nous permet dans notre exemple de faire un petit affichage pour marquer l’avancement vers l’explosion de la bombe.

Une petite précision concernant la fonction Tick() : une fois lancée, il n’est pas possible d’arrêter la génération des ticks configurée par l’appel à la fonction Tick(). Enfin, il est possible de configurer des timers qui généreront un seul événement à travers un channel. En utilisant ces timers, notre programme d’exemple deviendrait alors le programme suivant :

01: package main 02: 03: import ( 04: "fmt"

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 77

Pour aLLer PLus LoiN Marre de La routiNe ? Passez à La coNcurreNce aVec Les GoroutiNes !

05: "time" 06: ) 07: 08: func main() { 09: tick1 := time.NewTimer(1*time.Second) 10: tick2 := time.NewTimer(2*time.Second) 11: tick3 := time.NewTimer(3*time.Second) 12: tick4 := time.NewTimer(4*time.Second) 13: boom := time.NewTimer(5*time.Second) 14: for { 15: select { 16: case <-tick1.C: 17: fmt.Println("Dans 4 secondes, ce message s’auto-détruira") 18: case <-tick2.C: 19: fmt.Println("Dans 3 secondes, ce message s’auto-détruira") 20: case <-tick3.C: 21: fmt.Println("Dans 2 secondes, ce message s’auto-détruira") 22: case <-tick4.C: 23: fmt.Println("Dans 1 secondes, ce message s’auto-détruira") 24: 25: case <-boom.C: 26: fmt.Println("BOOM!") 27: return 28: default: 29: fmt.Println(".....") 30: time.Sleep(1*time.Second) 31: } 32: } 33: }

On voit ici que l’on définit quatre Timers différents (lignes 9 à 12), qui s’activeront les uns après les autres. Une petite subtilité présente dans chacune des balises case (lignes 16, 18, 20 et 22) correspondant à un des timers : le channel présent dans un timer se nomme C.

Enfin, si vous préférez créer des Tickers(), vous pourrez le faire en utilisant la fonction NewTicker(d Duration) * Ticker. Là aussi, le type Ticker contient un channel C, c’est d’ailleurs la seule donnée qu’il contient. L’intérêt de définir ses propres Tickers c’est que cela permet tout d’abord d’en avoir plusieurs et ensuite de pouvoir les arrêter lorsqu’ils ne sont plus nécessaires, grâce à la fonction (t *Ticker) Stop().

3.2 range et closeIl peut être utile de pouvoir lire dans un channel tant que

celui-ci n’est pas fermé. On peut faire cela avec le mot-clé range. C’est le mot-clé close qui vous permettra de fermer un channel.

Une précision à propos de close : seul le côté qui envoie des données dans le channel peut fermer celui-ci. Le côté qui réceptionne les données ne peut pas le faire. Enfin, tenter d’écrire à nouveau dans un channel que vous avez fermé générera un Panic. Voici un petit exemple de l’utilisation de ces deux mots-clés :

01: package main 02: 03: import ( 04: "fmt"05: "time" 06: ) 07: 08: func test_range(c chan int) {

09: for i := range c { 10: fmt.Println(i) 11: } 12: } 13: 14: func main() { 15: channel := make(chan int) 16: tab := []int{1, 2, 3, 4, 5, 6} 17: go test_range(channel) 18: for _, v := range tab { 19: channel <- v 20: } 21: close(channel) 22: time.Sleep(3*time.Second) 23: 24: }

Ce petit programme d’exemple est simple : il se contente de parcourir un tableau est d’envoyer chaque élément de celui-ci à notre goroutine test_range(). Celle-ci ne fait qu’afficher ce qu’on lui envoie à travers le channel et cela jusqu’à ce que celui-ci soit fermé. Pour cela, on utilise range à la ligne 9, directement dans la boucle for. Le close est quant à lui utilisé en ligne 21.

4 autour des goroutines et de la concurrence

Go fournit quelques paquetages intéressants autour des goroutines, ou tout simplement pour faire de la concurrence de manière plus classique.

4.1 Le paquetage runtimeLe paquetage runtime [5] vous permet d’interagir et

de contrôler le runtime de Go. Vous allez donc pouvoir agir directement sur l’exécution de vos goroutines. Vous allez, par exemple, y trouver la fonction Goexit(), qui vous permet de stopper la goroutine qui la lance. Une fonction intéressante est la fonction Gosched(). Celle-ci va céder l’exécution à une autre goroutine. NumGoroutine() int vous renverra, elle, le nombre de goroutines utilisées par votre programme.

4.2 Le paquetage syncLe paquetage sync [6] va mettre à votre disposition les

primitives de base classiques qui vous permettront de faire de l’exclusion mutuelle. Vous allez bien entendu disposer d’un Mutex qui pourra, de manière très classique, être pris ou relâché grâce aux fonctions (m *Mutex) Lock() et (m *Mutex) Unlock().

Concernant les Mutex, d’une manière très pragmatique, on se rend compte que l’on a souvent besoin de Mutex qui permettent une lecture multiple, mais une écriture unique. Go vous propose donc un RWMutex. Ce type de Mutex peut être pris par un nombre quelconque de lecteurs, mais par une seule demande d’écriture. Les fonctions concernant les lecteurs sont alors préfixées d’un R, soit RLock et Runlock.

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com78

Pour aLLer PLus LoiN ...Passez à La coNcurreNce aVec Les GoroutiNes !

Vous allez aussi disposer d’un objet Once, qui possède une fonction Do() qui prend un unique paramètre de type fonction. Lancer Do() a le résultat suivant :

- vous n’avez jamais lancé Do() encore, la fonction que vous donnez en paramètre s’exécute ;

- vous avez déjà lancé Do(), il ne se passe rien.

Vérifions cela avec un exemple :

01: package main 02: 03: import ("fmt" 04: "sync") 05: 06: func OneTest() { 07: fmt.Println("un test de Once.Do") 08: } 09: func main() { 10: var once sync.Once 11: once.Do(OneTest) 12: once.Do(OneTest) 13: }

La sortie de ce programme d’exemple est :

jmad@Albert:~/Code/Go/Examples$ goplay test_once_do.go un test de Once.Do

On voit bien qu’effectivement, notre fonction OneTest() n’a été lancée qu’une fois.

Également défini dans le paquetage sync, le type WaitGroup permet de mettre en place des mécanismes d’attente entre goroutines. D’une manière toute simple, on appelle la fonction Add() pour ajouter une valeur au comp-teur d’attente. Cette valeur peut d’ailleurs être négative ; dans ce cas-là, le compteur sera décrémenté. Ensuite, on lance les goroutines que l’on souhaite attendre. Une fois leurs exécutions terminées, chaque goroutine attendue lance alors la fonction Done(), qui va décrémenter le compteur d’attente. Une fois que ce compteur est inférieur ou égal à zéro, toutes les goroutines qui attendaient sont relâchées. Pour bien comprendre le fonctionnement des WaitGroups, nous allons réécrire l’un de nos premiers exemples d’utilisation de channels avec un WaitGroup. Cela donne le code suivant :

01: package main 02: 03: import "fmt" 04: import "time" 05: import "sync" 06: 07: func InspectorGadget() { 08: seconds := 10 09: time.Sleep(time.Duration(seconds)*time.Second) 10: } 11:12: func main() { 13: var rendezvous sync.WaitGroup 14: fmt.Println("Inspecteur gadget, arrêtez les sbires de Dr.MAD !") 15: rendezvous.Add(1) 16: go func() { 17: InspectorGadget() 18: rendezvous.Done()

19: }() 20: rendezvous.Wait() 21: fmt.Println("Les méchants sont sous les verrous !!") 22: }

Enfin, sync vous offre un mécanisme de condition (avec le type Cond) très simple, que là aussi vous allez pouvoir utiliser pour modéliser des rendez-vous. Une fois votre objet Cond créé, vos goroutines pourront se bloquer dessus et vous pourrez soit toutes les libérer d’un coup, avec un appel à Broadcast(), soit n’en réveiller qu’une en utilisant Signal().

Le paquetage sync contient un paquetage très intéressant, le paquetage atomic [7]. Celui-ci vous offre un certain nombre de fonctions de bas niveau, qui sont garanties comme étant atomiques. Typiquement, cela va être des fonctions d’ajout de int32 ou int64, de comparaison de valeurs (ou de pointeurs) ou de modification de valeurs de variables. Ce sont des opéra-tions toutes simples, mais le fait que le Go vous certifie qu’elles soient atomiques vous permet de ne pas avoir à faire ce que l’on fait dans un certain nombre de langages : des fonctions englobantes, qui permettent d’ajouter ce critère d’atomicité.

ConclusionOn dit souvent que la première impression est primordiale. Ma

première impression concernant les goroutines était excellente. Ce fut d’ailleurs une des choses qui m’a fait m’intéresser au Go, cette gestion à la fois originale, simple et très bien pensée de la concurrence. J’en vois certains, qui en lisant ma dernière phrase, ont levé les yeux au ciel en lisant que les goroutines et leur mécanisme de communication par channels étaient originaux. En effet, dans un nombre important de langages, on se retrouve, lorsque l’on fait de la concurrence en utilisant des threads, à mettre en place ce type de méthode de communica-tion. Mais là, l’originalité est vraiment de l’avoir intégrée dans un des paradigmes de base du langage. Alors bien entendu, les channels ne font pas tout. C’est un peu comme toutes les bonnes choses, et les design patterns, il faut savoir en user mais ne surtout pas en abuser. Et dans de nombreux cas, un bon vieux sémaphore ou un simple mutex fera totalement l’affaire. Mais cela n’en reste pas moins, à mon avis, un excellent outil qui dans de nombreux cas vous facilitera les choses, tout en vous épargnant de nombreuses heures de débogage.

Liens[1] Paquetage time : http://golang.org/pkg/time[2] Paquetage sort : http://golang.org/pkg/sort[3] Paquetage big : http://golang.org/pkg/math/big[4] Paquetage rand : http://golang.org/pkg/crypto/rand[5] Paquetage runtime : http://golang.org/pkg/runtime[6] Paquetage sync : http://golang.org/pkg/sync[7] Paquetage atomic : http://golang.org/pkg/atomic

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 79

cas sPécifiques

InTerfaCe GraPHIqUe en GTK+ par Tristan Colombo

GTK+, nom issu de GIMP Toolkit, est une bibliothèque permettant de réaliser des interfaces graphiques (elle est utilisée dans GIMP, GNOME, etc.). Différents éléments

graphiques, appelés widgets, sont disponibles et permettent de créer une fenêtre graphique par ajouts successifs d’éléments. On peut ainsi ajouter des boutons, des zones de saisie de texte, des zones de dessin, etc. Ces éléments peuvent être combinés entre eux : ajout d’une image dans un bouton par exemple.

Un paquetage Go est en cours d’écriture pour pouvoir utiliser les fonctions de cette bibliothèque. Au moment où ces lignes sont écrites, environ un tiers des widgets de GTK+ sont utilisables.

Lien : https://github.com/mattn/go-gtk

InstallationPour pouvoir utiliser le paquetage go-gtk, vous devrez avoir

préalablement installé les outils de développement de GTK :

sudo aptitude install libgtk2.0-dev

L’installation du paquetage est ensuite classique :

sudo $GOROOT/bin/go get github.com/mattn/go-gtk/gtk

exempleL’exemple présenté ici affiche une fenêtre très simple

contenant quatre éléments graphiques : un menu, un label, une image et un bouton (Fig. 1). Il s’agit d’une simplification de l’exemple proposé en démonstration avec le paquetage (fichier example/demo/demo.go).

Fig. 1 : Fenêtre graphique en GTK+ réalisée depuis un code en Go.

01: package main 02: 03: import ( 04: "fmt" 05: "github.com/mattn/go-gtk/glib" 06: "github.com/mattn/go-gtk/gtk" 07: ) 08: 09: func main() { 10: var menuitem *gtk.GtkMenuItem 11: 12: gtk.Init(nil) 13: 14: window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL) 15: window.SetPosition(gtk.GTK_WIN_POS_CENTER) 16: window.SetTitle("GTK en Go dans GLMF") 17: window.Connect("destroy", func(ctx *glib.CallbackContext) { 18: gtk.MainQuit() 19: }, "empty") 20: 21: // Conteneur principal22: vbox := gtk.VBox(false, 1) 23: 24: // Menu25: menubar := gtk.MenuBar() 26: vbox.PackStart(menubar, false, false, 0) 27: 28: cascademenu := gtk.MenuItemWithMnemonic("_File") 29: menubar.Append(cascademenu) 30: submenu := gtk.Menu() 31: cascademenu.SetSubmenu(submenu) 32: 33: menuitem = gtk.MenuItemWithMnemonic("E_xit") 34: menuitem.Connect("activate", func() { 35: gtk.MainQuit() 36: }) 37: submenu.Append(menuitem) 38: 39: // Conteneur40: vpaned := gtk.VPaned() 41: vbox.Add(vpaned) 42: 43: // Image44: imagefile := "src/LinuxMagazine.jpg" 45: image := gtk.ImageFromFile(imagefile) 46: vbox.Add(image) 47: 48: // Label49: label := gtk.Label("Presentation de go-gtk") 50: label.ModifyFontEasy("ChunkFive 40") 51: vbox.Add(label) 52: 53: // Bouton54: button := gtk.ButtonWithLabel("Un bouton") 55: button.Clicked(func() { 56: fmt.Printf("Clic sur bouton de label ‘%s’\n", button.GetLabel()) 57: }) 58: vbox.Add(button) 59: 60: // Fenêtre et boucle événementielle61: window.Add(vbox) 62: window.SetSizeRequest(1000, 600) 63: window.ShowAll() 64: gtk.Main() 65: }

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

aCCès à Une base De DonnÉes MysqL par Tristan Colombo

Pour accéder à un SGBD (Système de Gestion de Bases de Données), il faut forcément un driver capable d’établir un dialogue entre le code et la base. Il existe de nombreux SGBD et de nombreux paquetages sont disponibles pour chacun d’eux. Le plus compliqué est de faire un choix. Au niveau base de données, je vous propose d’utiliser une

base MySQL (ou mieux, MariaDB, fork sous licence GPL de MySQL et entièrement compatible avec cette dernière). Pour le paquetage Go, j’ai choisi le paquetage mymysql, car il s’agit de celui qui est le mieux maintenu et l’un des rares disposant d’une version stable (depuis le 26 septembre).

Lien : https://github.com/ziutek/mymysql

InstallationAttention, il faudra exécuter trois commandes pour installer tous les sous-paquetages de mymysql :

sudo $GOROOT/bin/go get github.com/ziutek/mymysql/thrsafe sudo $GOROOT/bin/go get github.com/ziutek/mymysql/autorc sudo $GOROOT/bin/go get github.com/ziutek/mymysql/godrv

exempleDans cet exemple, nous nous connecterons à une base contenant une table nommée GLMF et ne contenant que des

champs de type entier : l’identifiant id en auto-incrément, et une valeur value. Nous allons exécuter une requête permet-tant d’obtenir la liste des valeurs supérieures à 10.

01: package main 02: 03: import ( 04: "fmt" 05: "github.com/ziutek/mymysql/mysql" 06: _ "github.com/ziutek/mymysql/native" 07: ) 08: 09: func main() { 10: // Connexion à la base11: const ( 12: USER = "nom_d_utilisateur" 13: PASSWORD = "mot_de_passe" 14: DATABASE = "nom_de_la_base" 15: SERVER = "127.0.0.1:3306" 16: ) 17: db := mysql.New("tcp", "", SERVER, USER, PASSWORD, DATABASE) 18: 19: err := db.Connect() 20: if err != nil { 21: panic(err) 22: } 23: 24: // Requête dans la base25: rows, res, err := db.Query("select * from GLMF where value > %d", 10) 26: if err != nil { 27: panic(err) 28: } 29: 30: // Utilisation des résultats31: for _, row := range rows { 32: value := row.Int(res.Map("value")) 33: fmt.Printf("Champ ‘value’ vérifiant la requête : %d\n", value) 34: } 35: }

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com80

cas sPécifiques

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

www.gnulinuxmag.com GNU/Linux Magazine France Hors-Série N°63 81

cas sPécifiques

DÉVeLoPPeMenT web par Tristan Colombo

Nous allons voir un exemple de développement web en Go pur. Il est toutefois assez fastidieux de

développer un projet web sans framework. Python dispose de l’excellent Django, en PHP on peut utiliser Symfony, avec Ruby il y a Rails, ... et en Go ? Go propose notamment go-start. Bien sûr, chaque langage ne dispose pas d’un unique framework et il faut faire un choix à un moment donné. go-start me paraît être le meilleur choix actuellement, sachant qu’aucun framework Go n’est disponible en version stable. Il s’agit d’un framework MVC (Modèle-Vue-Contrôleur), disposant d’un système de paquetages additionnels, dont les modèles sont définis à l’aide de simples structures et qui utilise une base de données « NoSQL » MongoDB comme base par défaut. Comme avec toute version instable, attendez-vous à rencon-trer quelques problèmes si vous l’utilisez, notamment au niveau de la documentation que l’on ne peut même pas qualifier d’in-complète, car ça signifierait qu’il en existe une... Vous aurez tout juste accès à la godoc des paquetages. Voilà pourquoi nous allons rester sur un exemple de développement sans framework.

InstallationPas d’installation particulière à effec-

tuer ici.

exempleNous allons afficher une page qui sera

générée à partir d’un modèle ou template. Ce template pourra être personnalisé à l’aide de deux champs Title et Message. Il y aura ici deux fichiers, le premier étant le template index.tpl :

01: <!doctype html> 02: 03: <html lang="fr"> 04: <head> 05: <meta charset="utf-8" /> 06: <title>{{.Title}}</title> 07: </head> 08: 09: <body> 10: <h1>{{.Message}}</h1> 11: </body> 12: </html>

Le second fichier est le serveur web.go :

01: package main 02: 03: import ( 04: "net/http" 05: "html/template" 06: ) 07: 08: // Structure définissant les variables de notre template09: type Page struct{ 10: Title string 11: Message string 12: } 13: 14: func (p Page) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15: // Définition des valeurs à utiliser dans le template16: index := Page{ Title : "Page d’index", Message : "GLMF HS Go" } 17: 18: // Préciser le chemin complet vers les fichiers de template ou alors19: // lancer le programme serveur depuis le répertoire contenant les20: // templates21: t, err := template.ParseFiles("index.tpl") 22: if err != nil { 23: panic(err) 24: } 25: 26: // Génération de la page depuis le template et écriture dans w (serveur Web)27: // Pour des tests, w peut être remplacé par os.Stdout28: err = t.Execute(w, index) 29: if err != nil { 30: panic(err) 31: } 32: } 33: 34: func main() { 35: var p Page 36: 37: // Lancement du serveur38: http.ListenAndServe("localhost:8000", p) 39: }

Liens▪ http://jan.newmarch.name/go/template/

chapter-template.html

▪ http://golang.org/doc/articles/wiki/

À propos de go-start :

▪ https://github.com/ungerik/go-start

▪ http://lanyrd.com/2012/an-evening-with-go/spwbm/

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

GNU/Linux Magazine France Hors-Série N°63 www.gnulinuxmag.com82

cas sPécifiques

UTILIser Les arGUMenTs De La LIGne De CoMManDes

par Tristan Colombo

Go fournit un paquetage standard permettant de récupérer et de traiter des arguments passés depuis la ligne de commandes (on parle souvent

de mode CLI pour Command Line Interface). Ce paquetage se nomme flag et il est très simple à utiliser, mais ses pos-sibilités sont assez réduites. Je vous propose donc d’utiliser à la place le paquetage go-flags, qui permet notamment de différencier les options acceptant ou non des valeurs, les noms courts et les noms longs d’options, les options auxquelles des valeurs par défaut ont été assignées, de générer automatiquement l’aide (option -h ou --help), etc.

Lien : http://go.pkgdoc.org/github.com/jessevdk/go-flags

InstallationL’installation est on ne peut plus classique :

sudo $GOROOT/bin/go get github.com/jessevdk/go-flags

exempleDans cet exemple, nous autoriserons l’utilisation de

quatre options : -v, -n, -c et --activeFunction :

01: package main 02: 03: import ( 04: "fmt" 05: "github.com/jessevdk/go-flags" 06: ) 07: 08: func main() { 09: var opts struct { 10: // Option -v ou --verbose sous forme de booléen11: // Cette option est inactive par défaut (default:"false")12: Verbose bool `short:"v" long:"verbose" description:"Mode d’affichage verbeux" 13: default:"false"` 14: 15: // Option -n ou --number sous forme d’entier16: Number int `short:"n" long:"number" description:"Nombre à traiter"` 17: 18: // Option -c sous forme de tableau de booléens (permet de compter les appels)19: // Le type attendu étant un tableau, l’option peut être utilisée20: // plusieurs fois21: Count []bool `short:"c" description:"Compteur"` 22: 23: // Option --activeFunction déclenchant l’exécution d’une fonction de24: // rappel25: Callback func(string) `long:"activeFunction" description:"Exécute la fonction " +

26: "opts.Callback()"` 27: } 28: 29: // Définition de la fonction de rappel30: opts.Callback = func(msg string) { 31: fmt.Println("Fonction Callback() appelée avec le paramètre : " + msg) 32: } 33: 34: // Lecture des arguments de la ligne de commandes35: _, err := flags.Parse(&opts) 36: 37: if err != nil { 38: panic(err) 39: } 40: 41: // Affichage des données récupérées42: fmt.Printf("État de l’option Verbose : %t\n", opts.Verbose) 43: fmt.Printf("État de l’option Number : %d\n", opts.Number) 44: fmt.Printf("Nombre d’options Count : %d\n", len(opts.Count)) 45: }

Exemple d’exécution :

login@server:~$ flags -n 5 -v --activeFunction GLMF -c -c -cFonction Callback() appelée avec le paramètre : GLMF État de l’option Verbose : true État de l’option Number : 5 Nombre d’options Count : 3

login@server:~$ flags -hUsage: flags [OPTIONS]

Help Options: -h, --help Show this help message

Application Options: -v, --verbose Mode d’affichage verbeux -n, --number Nombre à traiter (0) -c Compteur ([]) --activeFunction Exécute la fonction opts.Callback()

panic: Usage: flags [OPTIONS]

Help Options: -h, --help Show this help message

Application Options: -v, --verbose Mode d’affichage verbeux -n, --number Nombre à traiter (0) -c Compteur ([]) --activeFunction Exécute la fonction opts.Callback()

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35

Plus d’informations sur : diamond.izibookstore.com

Achetez le magazine en PDF et feuilletez-le sur votre ordinateur, votre tablette ou votre smartphone !

et retrouvez GNU/Linux Magazine, MISC, Linux Pratique et Linux Essentiel en version PDF !!!

Venez découvrir NOTrE kiOSqUE NUMÉriqUE !

ReNDeZ-VoUS SUR

diamond.izibookstore.com

Ce

docu

men

t est

la p

ropr

iété

exc

lusi

ve d

e Je

an-F

ranç

ois

Rou

ceau

(ba

lam

.web

@gm

ail.c

om)

- 01

avr

il 20

13 à

19:

35