Go, routines et canaux - introduction au langage

Il y tellement de nouveaux langages, quand un de ceux là sort du lot c'est en général parce qu'il offre des concepts particuliers. Go est un de ceux là, et les concepts de routines, de canaux et de fonctions différées ne sont qu'une infime partie des possibilités que vous pouvez découvrir dans ce langage. Testons un peu ce langage.

J’ai la chance d’aimer mon métier, et dans ce métier on apprend souvent de nouvelles choses, des concepts, des langages... Dans le panel de langages informatique que j’ai put entrevoir, certains m’ont vraiment surpris dans leur concept. Le premier dont j’ai eut beaucoup de mal à m’approcher est le Haskell, et depuis peu je me penche sur Go. Je m’empresse donc de vous montrer un peu l’intérêt que je vois dans ce langage.

C’est Nicolas Engel, avec qui j’ai travaillé sur un projet, qui m’en a reparlé et l’envie de me repencher sur Go s’est vite manifesté.

Alors Go c’est quoi ?

Ce langage a été créé par une équipe de Google depuis 2010 et a pour vocation de réduire des problématiques que beaucoup de développeurs doivent supporter dans d’autres langages. Vous pouvez utiliser le compilateur Go à télécharger ici: http://golang.org/ ou utiliser gcc-go (package Fedora disponible dans les dépots officiels)

C’est un langage compilé, performant et sous licence BSD... La seule complication, et vous allez voir qu’elle est dominante au début, c’est que le concept est vraiment différent de pas mal de langages. Amis développeurs Python, Java, PHP et même C/C++... vous allez être un peu surpris.

La raison principale de la création de ce langage a été motivée par la réduction de problématiques telles que les threads, les sémaphores, les transferts interprocessus, les fractures mémoire, et l’impérativité des langages communs. Vous allez voir que des concepts ont été ajoutés à Go pour rendre tout cela vraiment très simple... si si, c’est finalement très simple.

Tout d’abord, c’est un langage fortement typé, les habitués de langages typés dynamiquement vont devoir se faire une raison: il va falloir déclarer vos variables. Ensuite, Go n’est pas un langage purement objet mais vous allez facilement pouvoir déclarer des struct avec des méthodes attachées. J’entends d’ici pas mal d’entre vous rire doucement sur un fond de “huhu aujourd’hui faire un langage non objet... c’est juste revenir dans le passé, autant faire du C”. Je tiens à donner mon avis là dessus, les langages orientés objet ont leurs avantages certains, mais Go est surtout fait pour être performant dans un premier lieu, et le C n’a rien de vieillot car même votre Linux, votre Mac ou votre Windows est un système d’exploitation quasiment entièrement écrit en C.

Enfin, pour ma part, je n’ai aucun mal à imaginer développer une grosse infrastructure logiciel avec un langage structuré non objet. Et surtout si c’est en Go. Dans tous les cas, Go est adressé aux développeurs désirant un langage proche du processeur, un peu comme C/C++ tout en gardant des simplification en terme de threads, sémaphores et gestion mémoire. Bien que Go permette de créer aussi des application web, et se retrouve même dans Google AppEngine. Voyez le guide de démarrage: go appengine.

Bref, passons la polémique trollesque et passons au langage lui même.

On commence par installer le compilateur. Soit vous utilisez gcc-go (dans les dépots) soit vous utilisez le compilateur de google à cette adresse: Golang. Je vous conseille, pour commencer, d'utiliser celui de google et de suivre la manière dont on utilise le runtime. En général, cela revient à faire la commande

go run monfichier.go

Pour le compiler réellement:

go build monfichier.go

Je ne vais pas vous détailler les bases du langage, mais un exemple rapide pour vous donner la syntaxe de base:

package main
 
//fmt est le package de formatage de chaine
import “fmt”
 
//on retrouve une fonction main
func main () {
     fmt.Println(“Hello le mondo !);
}

Bon rien de bien compliqué, un hello world bateau. Vous remarquez donc qu’on a une notion de package, et donc qu’on pourra séparer des fonctions et structures dans différent package (ce qui est un plus quand on se réfère au C). Cela ressemble fortement aux espace de nom (namespace) mais l’idée est plus claire selon moi.

Petite parenthèse, vous allez rapidement vous rendre compte au fur et à mesure que la plupart des concepts en Go se retrouve d’une manière ou d’une autre dans C.

Alors y’a quoi de nouveau ?

D’abord, comment on déclare une variable ? et bien de deux façons:

// i variable de type int
var i int
i = 4
 
//ou bien
//i est une entier valant 4, le typage est déduit par inférence
i:=4

Pour faire simple, utiliser ":=" (oubliez pas les deux points) pour déclarer en assignant, sinon l'assignation se fait avec un simple "="

Voilà, pas plus pas moins... on va pas parler de pointeur et de structure de suite, mais vous n’aurez pas de mal à comprendre en lisant la documentation ou en faisant le gotour (voir les liens en fin de billtet)

Je vais vous lister 3 concepts qui m’ont plut dans ce langage. Le premier est le concept de “goroutine” qui est foncièrement parlant une méthode pour créer des threads sans se prendre la tignasse et se taper le front sur le clavier. Le second concept est le principe de canal (channel) qui permet tellement de choses qu’on s’attriste de ne pas les avoir dans les autres langages (du moins pas sous cette forme). Un canal permet de faire transiter des valeurs d’un processus à l’autre, de bloquer un processus tant qu’un autre n’est pas terminé et en plus de bufferiser tout ça. C’est le concept de sémaphore (mutex) amélioré dans sa conception. Le troisième concept est la possibilité de différer un appel dans le temps (defer). Cela rend le code lisible à souhait.

Un autre concept, que l’on retrouve un peu en python, est le fait de pouvoir retourner plusieurs valeurs de plusieurs types depuis une fonction. Ceux qui n’ont pas compris l’intérêt devrait penser simplement à cela: imaginez une fonction qui retourne un résultat et un code d’erreur. Plutôt que de devoir retourner une structure, ou un objet pour les langages qui le permettent, on retourne une liste simple de variables.

Bref, passons aux goroutines de suite.

L’appel à une routine se fait simplement via le mot clef “go”. La fonction sera appelée dans un thread (mais dans le même processus, on ne parle pas d’un fork ici, mais bien d’un thread) et rien d’autre n’est à faire !

Je ne vous ai pas encore parlé des canaux, donc cet exemple ne va pas fonctionner exactement comme on le veut, mais le concept est là. Ne cherchez pas si ça fonctionne mal chez vous.

package main
 
import "fmt"
 
func hello(you string){
    for i:=0; i<5; i++ {
        fmt.Println("hello you:" + you)
    }
}
 
func main (){
    go hello("Pierre")
    hello ("Paul")
}

Ce qui va vous donner (selon la manière dont les threads sont créé sur votre OS):

hello you:Paul
hello you:Pierre
hello you:Paul
hello you:Pierre
hello you:Paul
hello you:Pierre
hello you:Pierre
hello you:Pierre
hello you:Paul
hello you:Paul

Vous l’avez remarqué, j’ai parfois 3 fois “Pierre” qui apparait, parfois 2 fois “Paul”. Cela est le fait que la routine est géré selon les disponibilités du CPU.

Bon, on passe aux canaux. Là vous risquez de perdre un peu le fil, mais je vous assure que la capacité de ce concept est tout bonnement génial.

Un canal est un type dont le principe est d’empiler des valeurs et de bloquer le processus si on cherche à lire dedans, et que celui-ci si il est vide. C’est le principe du FIFO. L’intérêt est sa syntaxe aisée et lisible.

Il faut utiliser la fonction “make” (genre de malloc, mais orienté fabrique ou factory) pour créer un “chan”.

//canal prenant des entiers
c := make (chan int)

Un canal peut prendre n'importe quel type, entier, flottants, chaines... et même des structures.

Ensuite c’est plutôt simple... On utilise “<-” pour placer des valeurs dans le flux.

Donc:

//empile un entier (1) dans le canal
c <- 1

Et pour lire le canal

//bloque tant que rien n’est empilé dans c
//on peut récupérer la valeur empilée
int val := <-c2
//ou simplement attendre une écriture dans la canal
<-c2

Faisons simple, on va juste comprendre le principe du canal:

package main
 
import "fmt"
 
func routine(a, b chan int){
     i := <-a
     fmt.Printf("je viens de lire dans le canal: %d\n", i);
 
     //'main' attend toujours, je vais écrire dans le canal b
     //pour le débloquer
     b <- 0
}
 
func main(){
    a,b := make(chan int), make(chan int)
 
    //on écrit dans a
    a <- 2
    //on lance une fonction en parallèle
    //qui reçoit les deux canaux en paramètres
    go routine1(a,b)
 
    //on va attendre la fin de la routine
    //la routine doit écrire dans le canal b 
    //pour que cette instruction se débloque
    <- b
    fmt.Println("voilà, j'ai fini, b a été lut")
}

Pour le moment on s'amuse à débloquer des canaux en écrivant dedans. Sachez que vous pouvez fermer un canal avec la fonction "close" mais si vous en avez besoin, c'est que vous être dans un cas particulier. Car en fait, Go n'aime pas que des canaux restent ouverts avec une valeur bloquée et non lut... pensez à traiter correctement les canaux, sinon forcez la fermeture avec "close"

Voyons un autre exemple de code qui va fonctionner avec un canal d’entier. Un peu plus salé par contre...

package main
 
import "fmt"
import "time"
 
 
func thread1 ( sem, done chan int) {
    fmt.Println("je suis le thread 1, je commence à bosser")
 
    //grosse fonction qui prend du temps
    time.Sleep(1 * 1e9)
 
    //on débloque le sémaphore
    sem<-0
    //on travaille encore un peu
    time.Sleep(1 * 1e9)
    fmt.Println("je suis le thread 1, je préviens main que je termine")
    //je préviens que la routine est finie
    done<-1
}
 
//voir plus bas (dans thread2), 
//cette méthode va permettre
//une exécution différée
func send(c chan int, num int) {
    c <- num
}
 
func thread2 (sem, done chan int) {
    //autre manière de s'assurer qu'on écrit bien à la
    //fin de la fonction :
    //defer permet de différer l'appel à "send" 
    //à la fin de ma fonction
    defer send(done, 2) 
 
    fmt.Println("je suis le thread 2, je commence à bosser")
 
    //on attend la fin de thread 1
    <-sem
    fmt.Println("je suis le thread 2, on m'a débloqué")
 
    //on bosse encore un peu
    time.Sleep(2 * 1e9)
 
    //quant sleep a terminé, la fonction différée va
    //être exécutée
}
 
func main(){
    //sem va me permettre de gérer les bloquage et débloquage
    //des goroutines
    sem := make (chan int)
 
    //ce canal sera emplilé par la seconde routine pour dire
    //à la fonction main qu'il a terminé
    done := make (chan int)
 
    //on lance les deux routine en parallèle, elle vont communiquer
    //au traver de sem
    //on remplira "done" pour dire que le thread est terminé
    go thread1 (sem, done)
    go thread2 (sem, done)
 
    fmt.Println("je suis dans main, j'attend la fin en lisant le canal 'done'")
 
    //les routines vont écrire dans "done",
    //j'attend que ce soit le cas pour les deux threads
    //donc on attend 2 fois
    //la variable i récupérera la valeur envoyé dans le canal
    var i int
    i = <-done
    fmt.Printf("routine %d vient d'écrire dans le canal 'done'\n", i)
    i = <-done
    fmt.Printf("routine %d vient d'écrire dans le canal 'done'\n", i)
 
    fmt.Println("je suis dans la fonction main, on m'a débloqué")
 
}

Voilà ce que donne mon programme:

je suis dans main, j'attend la fin en lisant le canal 'done'
je suis le thread 1, je commence à bosser
je suis le thread 2, je commence à bosser
je suis le thread 2, on m'a débloqué
je suis le thread 1, je préviens main que je termine
routine 1 vient d'écrire dans le canal 'done'
routine 2 vient d'écrire dans le canal 'done'
je suis dans la fonction main, on m'a débloqué

Ce qui est exactement ce que je voulais. La fonction main s’est mise en attente, les deux threads on commencé à travailler en parralèle. La routine 1 déblique la routine 2, mais cette routine 1 fini certaines choses en même temps. Enfin, la fonction “main” remprend la main (non c’est pas un jeu de mot)...

Plutôt que de créer une fonction "send" qui n'est utilisée que dans le thread 2, on peut utiliser un système de closure. Car en effet, les fonctions en Go sont des closures. Revoyons le code du thread 2:

func thread2 (sem, done chan int) {
    //autre manière de s'assurer qu'on écrit bien à la
    //fin de la fonction :
    //on crée une closure exécutée en fin de fonction
    //notez que c est local à la closure, la closure prend "done"
    //en argument, donc c correspondra à done
    defer func (c chan int) {
        c <- 2 
   }(done)
 
    fmt.Println("je suis le thread 2, je commence à bosser")
 
    //on attend la fin de thread 1
    <-sem
    fmt.Println("je suis le thread 2, on m'a débloqué")
 
    //on bosse encore un peu
    time.Sleep(2 * 1e9)
 
    //quant sleep a terminé, la fonction différée va
    //être exécutée
}

Vous pouvez donc supprimer la fonction "send". On retrouve ici un concept de fonction "inline" interne à une fonction. En bref, la capacité qu'à Go à vous simplifier la vie est vraiment intéressante.

Bref, là je n’ai fait qu’une introduction à Go... je n’ai pas le recul ni l’expérience nécessaire pour aller plus dans le détail (bien que je m’amuse comme un fou avec des tests bourrins). Je vous conseille d’aller lire le “gotour” ici : GoTour. Je vous conseille de bien vous pencher sur les notions de "range", de retour de plusieurs variables depuis une fonction mais aussi sur les concepts de structure et fonctions de structure. Ce billet ne visait qu'à vous éclairer sur ce que peut vous apporter Go coté parallélisation.

Go est désormais utilisable dans appengine, vous pouvez aussi créer votre propre miniserveur HTTP très facilement avec le package (inclu) "net/http" et que des portage de Gtk, WX etc... ont vu le jour et fonctionnenent.

En ce qui concerne les deux compilateurs, le compilateur (et runtime) Go de Google compile de manière statique mais donne de superbe résultats en terme de performances. Vous pouvez utiliser gcc-go (dans les dépots Fedora du moins) qui vous permettra de compiler dynamiquement vos programmes (le compilateur go faisant de la compilation statique...)

Un dernier point, Go vous permet aussi de récupérer très aisément des librairies C et d’utiliser les fonctions impémentées dans ce langage.