Publié par
Il y a 11 mois · 16 minutes · Back

Go basics

langage GOGo est un langage de programmation open source développé par Google. Relativement jeune, le langage jouit néanmoins d’une popularité enviable et quelques projets open source importants comme docker, kubernetes, consul ou encore influxdb l’utilisent. Afin de découvrir cette technologie nous vous proposons aujourd’hui une nouvelle série d’articles. Au fil des publications, nous aborderons des thématiques précises mais, pour le moment, je vous propose une simple découverte du langage, de sa syntaxe et des modèles de programmation qu’il propose.

Généralités

Créé en 2009, ce langage est pensé dès le début pour adresser plusieurs problématiques propres à Google et à son infrastructure logicielle. On citera notamment les problèmes de temps de compilation et de maintenabilité. Conçu pour la réalisation de grands projets par de larges équipes, Go privilégie avant tout l’efficacité. Le leitmotiv est simple : « Less is more ». De cet effort de simplification résulte la disparition de beaucoup de fonctionnalités présentes en Java, C++ ou en C#. Dites adieu à l’héritage, aux génériques et à plusieurs concepts avec lesquels vous étiez familiers. Mais attention, ne pensez pas que Go ne fait que supprimer des choses ! Ces concepts sont tout simplement remplacés. Un exemple emblématique est celui de la programmation concurrente. En lieu et place du modèle basé sur les threads que l’on connait bien en Java, les développeurs de Go nous proposent une implémentation du modèle CSP au travers de deux concepts : les goroutines et les channels. Go présente aussi d’autres caractéristiques comme le support natif de la composition ou un ensemble d’outils out-of-the-box très complet.

En une phrase, Go est un langage de programmation concurrent, doté d’un ramasse-miettes, typé statiquement, compilé nativement avec link statique et dont l’objectif est d’être le plus simple et le plus efficace possible.

Éléments de langage

Voyons un petit tour d’horizon de la syntaxe.

Main et package

Pour commencer, tout programme Go débute par un main, ni plus, ni moins. Pas de framework, retour aux bases !

package main

import "fmt"

func main() {
 fmt.Println("Hello world!")
}

Voici donc le très classique « Hello world! ». A la première lecture on peut déjà faire plusieurs remarques :

  • Une syntaxe familière, proche du C,
  • Pas de point-virgule à la fin des instructions,
  • La fonction main doit obligatoirement se trouver dans le package main de son programme.

Penchons-nous un instant sur l’instruction d’import. La chaine de caractères ‘fmt‘ désigne ici un package et c’est là un point fondamental du langage car, en Go, le package est l’unité de compilation.

Élément primordial du langage, les packages permettent d’organiser le code en modules réutilisables. C’est aussi au niveau des packages que se définit la visibilité de nos types et de nos fonctions. Enfin, on peut dépendre de packages qui ne sont pas présents en local, l’outil go get nous permettant ensuite de résoudre cette dépendance avant la compilation.

import(
 "os/exec"
 "github.com/me/my-project/hello"
    "github.com/me/my-project/json"
 j "encoding/json"
)

Le nom d’un package correspond à son emplacement dans l’arborescence des fichiers source. Ainsi il ne peut y avoir qu’un seul package dans un répertoire donné. L’accès aux ressources du package se fait en utilisant la dernière partie du nom du package, comme ici : ‘fmt.Println(« Hello world! »)‘.

Lorsque l’on dépend de packages distants, il suffit de préfixer le chemin d’ import par le chemin racine du gestionnaire de source (ici un exemple fictif avec github).

Enfin, il peut arriver de faire face à une collision de nom de package (ici json). Dans ce cas, il est toujours possible de définir des alias sur chacun des imports.

package example
 
func IamPublic(){}
 
func iAmPrivate(){}

Comme énoncé précédemment, c’est au niveau des packages que l’on va pouvoir définir une visibilité. Deux valeurs sont possibles : publique ou privé. Aucun besoin de mot clé, la visibilité d’une fonction, d’une variable ou d’un type défini dans un package donné dépend de la casse de son nom :

  • Upper camel case : publique
  • Lower camel case : privée

Un exemple ici avec les fonctions ‘IamPublic‘ et ‘iAmPrivate‘.

Slices et maps

Très rapidement vient le besoin de travailler avec des ensembles de données. La plupart du temps ceci se fait à l’aide de tableaux ou de tables de hachage. Découvrons ensemble comment les utiliser en Go.

Le tableau de base est l’Array. Il s’agit, comme sont nom l’indique, d’un type tableau de taille fixe comme vous en connaissez déjà. L’exemple ci-contre montre quelques opérations de création, de lecture et d’écriture d’Array.

Cependant, il est assez rare de se servir directement d’un Array, on lui préfère la plupart du temps un Slice.

var arrayOfString [2]string
arrayOfString[0] = "toto"
 
arrayOfInt := [2]int{42,0}
fmt.Println("The answer to the Ultimate Question is %d", arrayOfInt[0])

Un slice est un tableau dynamique. Un slice peut être créé soit de façon littérale comme un array soit, comme quelques autres types, à l’aide du mot clé make. Dans ce dernier cas on précisera la longueur et, de manière optionnelle, la capacité.

var arrayOfString [2]string
arrayOfInt := []int{42,0}
arrayOfString := make([string], 2, 5)

Enfin, des fonctions sont fournies permettant de connaître l’état du slice, de l’étendre ou de le réduire. Pour plus d’informations, je vous conseille l’excellent article Go: slices and internals ainsi que cette page de wiki détaillant un certain nombre d’opérations possibles sur les slices avec les fonctions append et copy notamment.

len(arrayOfInt)
cap(arrayOfInt)
arrayOfInt = append(arrayOfInt, 43, 44)
arrayOfInt = append(arrayOfInt[:2], arrayOfInt[3:]...)
 

Le dernier type de structure qu’ il nous reste à découvrir dans cette partie est la map qui est une table de hachage.

Tout comme les slices, une map peut être créée à l’aide du mot clé make ou bien de façon littérale.

var xebiaMap map[string]string
xebiaMap = make(map[string]string)
xebiaMap := map[string]string{
       "blog":  "http://blog.xebia.fr/",
       "xebia": "http://www.xebia.fr/career.html",
}

On peut ensuite se servir assez simplement de ces structures grâce à des fonctions et/ou des idiomes ad hoc.

Ainsi on retrouve notre fonction len qui retourne la longueur actuelle de la map, une fonction delete, ainsi que des idiomes de lecture et d’écriture.

On retiendra ici la façon qu’on a de vérifier la présence d’une clé (une opération courante lorsque l’on manipule une table de hachage) qui montre un idiome très utilisé en Go : le retour multiple. Ici la deuxième valeur retournée est un booléen indiquant si la valeur est présente ou non. Si l’on ne souhaite pas utiliser la première valeur retournée, on peut la remplacer par un underscore.

Cet article vous permettra d’en savoir plus sur l’utilisation des maps.

urlBlog := xebiaMap["blog"]
xebiaMap["back"] = "http://www.xebia.fr/career-back.html"
lenght := len(xebiaMap)
delete(xebiaMap, "back")
_, present := xebiaMap["back"]
 

Les structures de contrôle

func myFunc(name string) {
       if name == "myName" {
              fmt.Println("this is me !")
       } else {
     fmt.Println("this is not me !")
    }
}

En Go, elles sont assez familières et finalement assez simples à comprendre. Les blocs de code sont délimités par les sempiternelles accolades, mais les conditions ne sont pas entre parenthèses.

Il y a cependant une petite subtilité à prendre en compte pour les blocs else et else if. Ceux-ci doivent être déclarés sur la même ligne que l’accolade fermant le bloc précédent sous peine d’écoper d’une erreur à la compilation.

Passons ensuite aux boucles. Celles-ci peuvent être plus déroutantes car si Go supporte les boucles for, while et for-each, il n’existe qu’un unique mot clé pour toutes les exprimer. Voyons comment faire en partant de la boucle for classique.

for i:= 0; i < 10; i ++ {
       fmt.Printf("iteration index : %d", i)
}

 La boucle for en Go s’exprime comme la boucle for du C avec une initialisation, une condition et un incrément.

for {
       fmt.Printf("Infinite loop !")
}

Une boucle for peut aussi s’écrire sans rien préciser sur son comportement. On a alors une boucle infinie. On est donc encore assez proche de ce que l’on peut trouver en C.

i := 1
for i < 10 {
       fmt.Printf("iteration index : %d", i)
    i += i
}

La boucle while est, quant à elle, une boucle for sans initialisation ni incrément. On ne retrouve alors que la condition. Notez cependant qu’ il n’existe en revanche pas de support du do...while en Go.

array := []string{"toto", "titi"}
for index, val := range array {
       fmt.Printf("it index : %d, iteration value : %s", index, val)
}
 

La boucle for-each est en revanche très largement supportée. Il est possible d’itérer sur les arrays, les slices, les maps, les strings et les valeurs lues à partir de channels.

Dans l’exemple ci-contre, nous itérons sur un slice de string. Le langage nous permet à la fois de manipuler les index et les valeurs.

array := []string{"toto", "titi"}
for index := range array {
       fmt.Printf("iteration index : %d", index)
}

Si la valeur n’est pas utilisée, elle peut être omise.

array := []string{"toto", "titi"}
for _, val := range array {
       fmt.Printf("iteration value : %s", val)
}

Si c’est l’index qui n’est pas utilisé, il est possible de le remplacer par un underscore.

str := "toto"
for _, char := range str {
       fmt.Printf("the parsed char is %q", char)
}

Le type string étant un slice en lecture seule, on peut itérer directement sur un string.

xebiaMap := map[string]string{
       "blog":  "http://blog.xebia.fr/",
       "xebia": "http://www.xebia.fr/career.html",
}
for key, val := range xebiaMap {
       fmt.Printf("iteration  key : %s, iteration value : %s", key, val)
}

Même principe lorsque l’on itère sur une map, à la différence près que l’index est ici remplace par la clé.

Les structures et les interfaces

Pour finir ce tour d’horizon de la syntaxe, tournons nous maintenant vers le système de type de Go. Ici vous ne trouverez ni classe, ni objet, ni héritage. Si Go n’est pas stricto sensu un langage orienté objet, il supporte nativement le polymorphisme, les interfaces, les méthodes…

La première forme de type que l’on va vraisemblablement utiliser lorsque l’on débute en Go est la struct. Il s’agit, comme son nom l’indique, d’une simple structure de données. Celle-ci contient des champs nommés.

Une structure peut être instanciée à l’aide de la fonction new, auquel cas les champs sont mis à zéro, ou bien de façon littérale en précisant des valeurs utilisées pour l’instanciation.

Il existe cependant une différence importante entre ces deux instanciations : lorsque l’on utilise la fonction new, la valeur retournée est un pointeur, alors qu’il s’agit d’une valeur dans le cas de l’expression littérale. Go, à l’instar de beaucoup de langages de programmation modernes, nous laisse le choix entre pointeur et valeur, mais sans arithmétique de pointeur.

type myType struct {
       fieldOne string
       fieldTwo string
}
 
tp := new(myType)
tv := myType{"one", "two"}

Une structure peut donc recevoir des méthodes. Cependant la cible (le récepteur) de ces méthodes n’est pas forcément directement une structure, mais peut être un pointeur sur cette structure, ce qui permet à la méthode de muter directement la structure cible.

func (t myType) printFieldOne() {
       fmt.Println(t.fieldOne)
}
 
func (t *myType) setFieldOne(val string) {
       t.fieldOne = val
}

Cependant, une structure représente une implémentation possible d’un comportement. En Go, un comportement ou un ensemble de comportements, s’exprime au travers d’interfaces.

type Duck interface {
       Quack()
}

Pour associer notre interface Duck à une implémentation, Go utilise du structural typing qui est du duck typing vérifié à la compilation, à savoir que n’importe quoi peut être considéré comme de type Duck, du moment que ce « n’importe quoi » possède la méthode Quack avec la même signature.

Pour en savoir plus sur les interfaces, je vous conseille cet article.

Il y aurait encore beaucoup à dire sur les types en Go ne serait-ce qu’avec la composition, le type switch ou bien encore le type assertion pour ne citer qu’eux. Nous y reviendrons plus tard au fur et à mesure des articles.

package main

import "fmt"

type Duck interface {
       Quack()
}

type Mallard struct {
}

func (Mallard) Quack()  {
       fmt.Println("QUACK QUACK QUACK !")
}

func doQuack(d Duck)  {
       d.Quack()
}

func main() {
       doQuack(Mallard{})
}

La programmation concurrente

Ce sujet mérite et aura un article dédié mais on ne peut pas écrire un article de présentation de Go et omettre ce sujet. Voici donc quelques éléments d’introduction à la programmation concurrente en Go.

Avant de présenter la moindre ligne de code, il est tout d’abord nécessaire de parler du modèle théorique sur lequel Go s’appuie : le modèle CSP.

Celui-ci est, à l’instar du modèle Acteur, un modèle où les processus s’exécutant en concurrence ne communiquent pas par partage de mémoire mais partagent de la mémoire en communiquant. Go utilise deux mécanismes clés pour implémenter CSP. Tout d’abord les Goroutines, qui sont des processus ultra légers, multiplexés sur quelques Threads et gérés par un scheduler, ensuite les Channels, qui permettent d’échanger des messages entre Goroutine.

package main

import "fmt"

func HelloGoroutines() {
       fmt.Println("Hello Goroutines !")
}

func main() {
       go HelloGoroutines()
}

Pour lancer une Goroutine, rien de plus simple. Le mot clé go suffit.

Pourtant un problème va survenir si vous essayez d’exécuter le code : rien ne s’affiche ! En réalité, le programme se termine avant que la goroutine exécutant la fonction HelloGoroutines n’ait eu le temps de faire quoi que ce soit.

package main

import "fmt"

var c chan bool

func HelloGoroutines() {
       fmt.Println("Hello Goroutines !")
       c <- true
}

func main() {
       c = make(chan bool)
       go HelloGoroutines()
       <- c
}

Pour résoudre ce problème, on peut utiliser un channel. Pour réaliser une opération de lecture sur un channel, on utilise l’opérateur <- avec à gauche la variable réceptionnant la valeur lue (il peut ne pas y en avoir) et à droite le channel. Pour une opération d’écriture, meme opérateur, mais le channel est à gauche et la valeur à écrire à droite.

On en déduit ici que les opérations de lecture et d’écriture sur un channel sont bloquantes. Cependant en Go bloquer ne veut pas dire bloquer un thread mais seulement bloquer une goroutine. Du point de vue du Thread, ces opérations sont asynchrones et une commutation de contexte au niveau des goroutines est assurée par le scheduler.

Il est toutefois possible de créer des channels non-bloquants. Il faut alors ajouter la taille du channel en tant que deuxième paramètre. La lecture est alors non bloquante tant que le channel contient des éléments. De même l’écriture est non bloquante tant que le channel n’est pas plein.

func someFunction(c chan string)  {
       select {
       case c <- "ping":
              fmt.Println("somebody is listening to me !")
       default:
              fmt.Println("I am a poor lonesome function :( ")
       }
}

Un autre idiome permet de « tester » le caractère bloquant d’une opération sur un channel et, le cas échéant, d’exécuter des actions alternatives à l’aide du mot clé select.

Ce que j’en pense

Les plus

En sus de ses qualités en termes de lisibilité et de programmation concurrente, Go a le bon goût d’être livré avec un outillage standard très fourni. En effet, en lieu et place d’un simple compilateur, c’est une véritable usine logicielle qui est mise à disposition du développeur : outils de formatage automatique, runner de tests et de benchmark automatisé, outils d’analyse et de couverture de code, outils de génération et de consultation de doc, un gestionnaire de dépendances… De plus son SDK est très riche, bien fait et moderne. Par exemple, la création d’un serveur HTTP est très simple et ne nécessite que très peu de lignes de code sans avoir recours à aucun framework.

L’approche globale de la programmation proposée par Go est très pragmatique et très simple aussi (la spécification du langage se lit très rapidement et facilement). Une syntaxe claire, un bon outillage, des concepts de programmation puissants mais simples, vous aurez compris que c’est un langage que j’apprécie beaucoup, même si il n’est pas exempt de défauts.

Les moins

On peut regretter que certains de ces outils puissent être, pour le moment, parfois (trop ?) limités.

Ainsi le gestionnaire de dépendances ne gère pas les versions ! Si un certain nombre d’outils peuvent être utilisés pour pallier ce manque, c’est assez dommage de devoir s’en remettre à des outils tiers plutôt qu’à l’outil officiel. Dans le même esprit, si Go fournit un runner de test, il n’y a en revanche aucune bibliothèque d’assertion ni de mock en standard (ici une excellente librairie signée Testify).

Enfin les idiomes de Go, aussi lisibles soient-ils, peuvent parfois paraître lourds lorsque l’on connait d’autres langages. Le langage se montre aussi très impératif et les amoureux de la programmation fonctionnelle risquent de ne pas y trouver leur compte. Enfin, certaines bonnes pratiques officielles notamment concernant le nommage des variables, paramètres et fonctions peuvent heurter la sensibilité des craftsmen.

Quelques liens

Une réflexion au sujet de « Go basics »

  1. Publié par bobby, Il y a 11 mois

    MErci pour cet article…j’apprécie également les qualités du langage après 15 ans à faire du Python.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *