GERBELOTBARILLON.COM

Parce qu'il faut toujours un commencement...

Langage Go

Quelques éléments factuels


Contrairement à la majorité des langages qui fleurissent sur nos systèmes informatiques, le langage Go est inspiré des langages C et Pascal, compilé et fortement typé. Il a été conçu par Robert Grisemr, Rob Pike et le fameux Ken Thopson, au sein de Google. Pour mémoire, Ken Thompson gravite dans la sphère informatique depuis 1966 et a cotoyé Dennis Ritchie, avec lequel il a participié à l'essor du système Unix. Il avait également déjà travaillé avec Rob Pike sur le codage UTF-8.

L'objectif de Go est d'être rapidement compilable et rapidement exécutable. L'empreinte mémoire est très faible et les temps de fonctionnement très proches de ceux du C, langage système par excellence. Ce langage évolue très vite avec des releases majeures tous les 6 mois et constamment des nouvelles bibliothèques qui enrichissent fonctionnellement le langage. Le langage est supporté par Google mais reste en environnement open source.

Un fichier compilé en Go génère un seul fichier. Cela rend très pratique le déploiement car l'usage d'installeur ou de setup n'est plus requis. Ce qui rend le langage intéressant également est qu'il ne cède pas aux sirènes de la programmation objet (python également tout en étant plus lent que Go) qui reste d'un abord très particulier et "weird". On retrouve également la gestion de la programmation parallèle (concurrent) ainsi qu'un garbage collector qui évitera les segfault et autres dumps quand la libération de mémoire n'est pas faite ou mal gérée (libération de mémoire utilisée par d'autres programmes par exemple).

Installation


Il suffit de se rendre sur le site officiel de Go (https://golang.org/dl/) et de procéder au téléchargement selon la plateforme voulue. Cela vous donnera accès au compilateur et à la toolchain pour travailler avec Go.

Au niveau de l'IDE, Go est livré sans éditeur de code et vous êtes donc libre d'utiliser celui qui vous convient le mieux. Cela peut aller du simple éditeur de texte du genre NotePad++ ou PSPad à Visual Studio Code, voire Goland qui se trouve être un IDE payant édité par JetBrains, éditeur bien connu pour ceux pratiquants d'autres langages comme Python ou Java. Il reste aussi un indétrônable : Emacs, le couteau Suisse de l'éditeur, disponible pour Windows sur le site FTP officiel du projet GNU -> http://ftp.gnu.org/gnu/emacs/windows/

Une fois l'installation de Go terminée, vous aurez accès à un ensemble de commandes mais la plus importante est celle qui vous permettra de compiler : go build prog.go (changez prog.go par le nom de votre fichier). Si vous disposez de plusieurs fichiers déclarés dans le même package, spécifiez-les dans la ligne d'exécution comme, par exemple, si vous avez un programme principal progmain.go et deux autres morceaux de code nommés code1.go et code2.go, go build progmain.go code1.go code2.go. Cela permettra au compilateur de recréer automatiquement ses dépendances sans erreur.

Pour obtenir une liste complète des combinaisons système d'exploitation / architecture supportées par votre compilateur, faites go tool dist list. Vous verrez alors que vous pouvez produire du code en cross compilation pour une plateforme javascript/Webassembly ou pour du freebsd sur arm, et bien d'autres. Pour produire de la cross compilation, ajoutez les variables d'environnement GOOS et GOARCH pour spécifier le comportement attendu du compilateur Go. En lançant la commande go env vous obtiendrez la liste des variables d'environnement actuellement utilisées et vous verrez que GOOS et GOARCH correspondent à l'environnement en cours de Go (windows et amd64 par exemple).

Hello world


Pour vous montrer rapidement comment est structuré un programme Go, rien de mieux que le traditionnel "Hello World" des familles...

package main

import "fmt"

func main() {
   fmt.Println("Hello World.")
}
Un programme Go a une structure obligatoire composée de 3 éléments :

Pour ajouter une fonction et assister au passage par paramètres typés, voici une fonction pour l'addition de deux valeurs. Tout doit être typé, des paramètres au retour de la fonction. Observons la syntaxe d'appel d'une fonction : func <nom de la fonction> ([<type> x]) <type de retour> {} Pour clarifier les choses, prenons un programme d'addition simple de deux entiers :

package main

import "fmt"

func add(int x, int y) int {
   return x + y
}

func main() {
   var i, j int = 40, 2
   fmt.Println(add(i, j))
}

Mémorisez bien ceci


La première instruction d'un programme Go doit être une notion de package. Par défaut ou par convention, il est de bon ton de nommer ce package main mais d'autres conseillent de lui faire simplement porter le nom du dossier dans lequel votre programme est créé (sans le chemin contrairement à Java).

Après la déclaration du package, vient la déclaration des dépendances. Ce sont des bibliothèques externes dans lesquelles vous allez pouvoir piocher des fonctions prêtes à l'emploi. Parmi les plus usitées se trouvent "fmt" et "os". ATTENTION : si vous déclarez un import mais que vous n'utilisez pas une seule de ses fonctions, Go refusera de compiler votre projet. Si plusieurs dépendances sont importées, il faut les noter l'une sous l'autre dans une commande d'import qui aurait alors cet aspect

import (
   "fmt"
   "os"
   "net"
)

Il existe un grand nombre de packages disponibles. Une liste détaillée ainsi qu'un ensemble très bien construit d'exemples se trouvent sur le site https://golang.org/pkg/. Si la caverne d'Ali Baba existe, c'est certainement là...

La fonction principale d'un programme, comme pour "C", est la fonction main. Elle se déclare par func main() {}. Pour utiliser des fonctions typées, procédez comme suit :

func <nom de la fonction> ([<type> x, ...]) <type de retour> {}

Autres points :

Eléments de langage


Types de variables

Comme dans la majorité des autres langages, il existe des types de bases entiers et flottants, signés et non signés. GoLang permet évidemment la déclaration des tels types avec pour moyen mnémotechnique au tout type commençant par 'u' est une valeur non signée (unsigned).

TYPE TAILLE VALEURS POSSIBLES
uint8 ou byte 8 bits 0 à 255
uint16 16 bits 0 à 65535
uint32 32 bits 0 à 4294967295
uint64 64 bits 0 à 18446744073709551615
int8 8 bits -128 à 127
int16 16 bits -32768 à 32767
int32 ou rune 32 bits -2147483648 à 2147483647
int64 64 bits -9223372036854775808 à 9223372036854775807
uint ou int 32 bits ou 64 bits voir int32, int 64, uint32 ou uint64
float32 32 bits 7 chiffres décimaux
float64 64 bits 16 chiffres décimaux

Variables booléennes

Une variable booléenne a pour but de posséder l'un des deux états suivants :

Un booléen est déclaré par le mot-clé bool.

Déclaration des variables

La déclaration de variables s'effectue en utilisant le mot-clé var. Vous pouvez déclarer une ou plusieurs variables en même temps sur la même ligne et forcer le type de la variable ou laisser Go le déterminer pour vous à la compilation.

var [nom_de_la_variable] [type]

Lorsqu'une variable est déclarée mais non initialisée, elle prend la valeur du zéro de son type à savoir 0 pour un entier, une chaine vide pour un string...

Un cas particuier et également un raccourci pour déclarer une variable : utiliser le symbole d'affectation := va réaliser à la fois la déclaration et l'affectation d'une valeur à une variable. C'est une notation raccourcie que l'on trouvera très souvent dans les déclarations des boucles for où l'on utilise un index mais qui n'a aucune autre utilité que de servir dans le parcours de la boucle.

package main

import "fmt"

func main() {
   var a = "hello" // déclaration d'une chaine
   var b, c int = 1, 2 // déclaration de deux valeurs entières avec initialisation
   var d = true // Go va déterminer le type booléen pour cette variable
   var e int // Go va affecter la valeur 0 à e
   f := "42" // création automatique d'une variable avec affectation
}
Déclaration de constantes

Une constante est déclarée avec le mot clé const et ne donne par défaut pas de type à une variable, à moins de le faire explicitement ou selon le contexte d'utilisation de cette constante.

package main

import (
   "fmt"
   "math"
)

func main() {
   const s string = "hello" // déclaration d'une constante de type chaine de caractères
   const n = 4200000 // déclaration sans type
   fmt.Println(int64(n)) // utilisation de la constante comme entier
   fmt.Println(Math.Sin(n)) // typage selon le contexte (donne un float64 pour Sin)
}
Les opérateurs

Les opérateurs de Go sont similaires à l'ensemble des autres langages de programmation :

Boucles

Le langage Go utilise le terme for pour manipuler les boucles. Par la même occasion il n'y a pas les autres constructions repeat, do...loop et while des autres langages. La construction de l'instruction for impose la présence du déclarateur de bloc (l'accolade) à la fin de la ligne (tout comme pour les déclarations de fonctions). Les formats de boucles For sont les suivants :

Exemple de boucle avec initialisation de variable préalable, avec condition

i = 3
for i <= 42 {
   fmt.Println(i)
   i = i+ 1
}

Exemple de boucle avec initialisation automatique de variable d'index par :=

for j := 1; j <= 42; j++ {
   fmt.Println(j)
}

Exemple de boucle infinie. Pour en sortir, il existe les fonctions break ou continue selon que l'on souhaite quitter la boucle ou terminer un tour de boucle par anticipation et reprendre un autre tour.

for {
   fmt.Println("Je suis une boucle infinie")
   break // seule façon de quitter la boucle
}
Exemple de boucle qui ira à son terme mais dont certains tours ne sont pas complets.
for n := 0; n <= 42; n++ {
   if n % 2 == 0 {
      continue
   }
   fmt.Println(n) // n'affiche que les nombres impairs
}

Les conditions

Les sélections par Si / Alors / Sinon sont très semblables aux autres langages à ceci près qu'il n'y a pas de paranthèses entourant la condition et que les accolades sont obligatoires (marqueurs de bloc).

if n % 2 == 0 {
   fmt.Println("Ce nombre est pair.")
} else {
   fmt.Println("Ce nombre est impair.")
}
if n < 0 {
   fmt.Println("valeur négative")
} else if n > 0 {
   fmt.Println("Valeur positive")
} else {
   fmt.Println("Valeur nulle")
} 

Une autre construction de tests en cascades est réalisable par l'instruction switch. C'est une structure plus élaborée que dans la majorité des autres langages avec des mélanges de tests sur valeurs ou types de variables, simples ou multiples. Si ce n'est pas clair, les exemples suivants devraient éclaircir les choses.

i := 3
switch i {
   case 1:
      fmt.Println("Un")
   case 2:
      fmt.Println("Deux")
   case 3:
      fmt.Println("Trois")
   case 4, 5, 6:
      fmt.Println("Autres chiffres")
   default:
      fmt.Println("valeur inconnue")
}

switch {
   case i = 42:
      fmt.Println("Vous avez le nombre de la vie")
   default:
      fmt.Println("Essayez encore")
}

Déclarer des fonctions


Une fonction est un moyen simple de structurer des blocs de code qui pourront être appelés de plusieurs endroits différents du programme. Une fonction est déclarée selon le prototype suivant :

func nomDeFonction (listeDesParamètres) typeDeRetour {
   /* Code de la fonction */
         }
Une fonction dispose d'un nom de fonction, qui doit être unique dans l'ensemble du programme. Par essence une fonction renvoie un ou plusieurs éléments. Il lui faut donc un type de retour. Une fonction peut accepter des paramètres sur lesquels elle va travailler.

Pour renvoyer des valeurs, la fonction doit utiliser la commande return suivie des items à retourner. Il peut y en avoir un nombre variable.

func add42(int a, int b) (int, int) {
   return a+42, b+42
         }

Il arrive parfois qu'une fonction accepte un nombre variable de paramètres. Ils se déclarent avec les points de suspension ... précédant le type des paramètres. Par exemple, la fonction suivante va permettre d'additionner un nombre variable de nombres.

package main

import "fmt"

func main() {
   fmt.Println(addition(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
   fmt.Println(addition(10, 20, 30))
}

func addition(param ...int) int {
   total := 0
   for _, value := range param { // on ne prend pas en compte l'index de la range avec '_'
      total += value
   }
   return total
}

Un cas particulier est la fonction anonyme. Elle ne dispose pas de nom, peut être utilisée comme paramètre et même affectée à une variable. Pour la passer en tant que paramètre, son type doit correspondre au prototype de cette fonction.

package main

import (
   "fmt"
   "math"
)

/*
 * Prend en paramètre un float et une fonction anonyme.
 */
func add42(a float64, anonyme func(float64) float64) {
   op := anonyme(a)
   res := op + 42
   fmt.Println(res)
}

func main() {
   racineCarree := func(x float64) float64 { return math.Sqrt(x) } // stockage dans une variable
   add42(10, racineCarree)

   // Appel direct sans passer par une variable
   add42(20, func(x float64) float64 { return math.Pow(x, 2) })
}

Les pointeurs en Golang


Toutes les données que vous manipulez lorsque votre ordinateur est allumé se trouvent dans la mémoire vive (la RAM). Chaque élément occupe une ou plusieurs cases en mémoire. Il est possible d'obtenir l'adresse de cette case ainsi que son contenu. Pour partie, c'est par l'intermédiaire des pointeurs que nous pourrons manipuler ces adresses mémoire.

Un pointeur se déclare comme suit :

var nom_de_la_variable *type_de_la_variable
. C'est l'usage de l'astérisque * précédant le type de votre variable que le compilateur Go va traiter la variable comme un pointeur mémoire d'un certain type.

package main

import (
   "fmt"
)

func main() {
   var a int = 42
   fmt.Println("L'adresse de la variable a est : %p\n", &a)

   var b *int // déclaration d'un pointeur sur un entier
   b = &a // fait pointer la variable b sur l'adresse de la variable a
   fmt.Println("La valeur de b est %d\n", *b) // Affiche la valeur de b qui est identique à celle de a

   a++ // change la valeur de a
   fmt.Println("La valeur de b est %d\n", *b) // La variable b a également changé de valeur
}

La différence formelle entre un pointeur et une variable est qu'une variable contient une valeur alors qu'un pointeur contient une adresse. Il est toutefois possible d'accéder à la valeur pointée en déréférençant le pointeur avec le symbole astérisque * placé devant la variable pointeur.

Comme tout type dans Go, le pointeur dispose d'une valeur par défaut : nil. Cela permet de savoir si un pointeur a été initialisé (une adresse est pointée par le pointeur) ou non.

Enfin, l'utilité des pointeurs est également rencontrée dans le passage de paramètres à des fonctions, paramètres devant être modifiés ou trop coûteux à passer. Pour rappel, les paramètres des fonctions en Go sont toujours passés par valeur, donc en provoquant une copie de l'élément avant transmission à la fonction. Cela bloque également la possibilité de modification de ce paramètre.

Dans ce cas la solution est la passage par référence, autrement dit par pointeur. Ainsi la valeur paramétrique pourra être modifiée et le transfert de grosses structures sera simplifié.

package main

import (
   "fmt"
)

func ajout(v *int) {
   *v += 42 // Ajoute 42 à la valeur passée en paramètre
}

func main() {
   var a int = 42
   fmt.Println("Valeur avant la fonction : %d\n", a)
   ajout(&a)
   fmt.Println("Valeur après la fonction : %d\n", a)
}

Les ensembles de données


Les tableaux

Pour Go, un tableau est une liste séquentielle d'éléments de même type. Lors de l'initialisation du tableau il est automatiquement rempli par le zéro du type soit 0 pour les entiers et une chaine vide pour les chaines. Son initialisation peut également être réalisée lors de sa déclaration en précisant les valeurs entre accolades. L'accès à une valeur d'une case du tableau est réalisé par les index de tableau et la longueur est donnée par la fonction len(). Un tableau peut être multi-dimensionnel.

var a [3]int // tableau vide
fmt.Println(a) // affiche le tableau sous la forme [0 0 0]

a[1] = 42
fmt.Println(a) // affiche [0 42 0]
fmt.Prinln(len(a)) // affiche la longueur du tableau soit la valeur 3

b := [4]int{1, 2, 3, 4} // initialise un tableau à la déclaration, sans var

var c [2][3]int // déclare un tableau vide bidimensionnel (rempli de 0) de deux lignes de 3 colonnes
for i := 0; i < 2; i++) {
   for j := 0; j < 3; j++ {
      c[i][j] = i + j
   }
}
fmt.Println(c) // affiche le tableau 2d sous la forme [[0 1 2][1 2 3]]

Les slices

Les slices sont une amélioration importante dans la gestion des séquences en Go. Pour créer un slice il faut utiliser l'instruction make qui va créer une slice initialisée avec les valeurs par défaut du type utilisé ou bien utiliser la syntaxe var standard ou bien avec l'affectation directe.

s := make([]string, 3)
fmt.Println(s) // affiche [   ]

s2 := []string{"ch1", "ch2", ch3"}
fmt.Println(s2) // affiche ["ch1" "ch2" "ch3"]

var s3 []int // création d'un slice d'entiers vides
fmt.Println(s3) // affiche []
Pour ajouter des éléments à une slice déjà initialisée il suffit d'utiliser l'instruction append().
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println(s) // affiche [a b c]
s = append(s, "def")
fmt.Println(s) // affiche [a b c def]
Il est également possible de réaliser des copies de slices par l'instruction copy(dst, src).
c := make([]string, len(s)) // crée un slice de la longueur du slice d'origine
copy(c, s)
fmt.Println(c) // affiche le contenu du slice [a b c def]
Pour ceux qui ont des connaissance en Python, les slices ont les mêmes comportement et permettent donc d'extraire des sous-ensembles par les notations [from:to], la valeur de to étant exclue de la réponse.
fmt.Println(s[2:4]) // affiche les données de s[2] et s[3]
fmt.Println(s[:4]) // affiche le contenu du slice jusqu'à s[3], s[4] est exclue
fmt.Println(s[1:]) // affiche le contenu du slice de s[1] à la fin du slice
Tout comme pour les tableaux, un slice peut s'initialiser avec des valeurs dès sa déclaration.
t := []string{"a", "b", "c"}
fmt.Println(t) // affiche [a b c]
Enfin la notion de slice multi-dimensionnel existe également sous la même forme que les tableaux.
2d := make([][]int, 3)
for i := 0; i < 3; i++ {
   inside := i + 1
   2d[i] = make([]int, inside)
   for j := 0; j < inside; j++ {
      2d[i][j] = i + j
   }
}
fmt.Println(2d) // affiche [[0] [1 2] [2 3 4]]

Les dictionnaires (maps)

Les dictionnaires sont des ensembles spéciaux composés de paires d'informations sous la forme clé/valeur ou key/value pour plus de facilité de compréhension. Pour créer un dictionnaire, il faut utiliser l'instruction make(map[type-pour-la-clé]type-pour-la-valeur). Il est également possible de réaliser l'affectation des valeurs lors de l'initialisation avec l'instruction similaire aux tableaux par m := map[key-type]value-type{"key1":value1, "key2":value2}.

Pour récupérer une valeur d'un dictionnaire, il suffit d'utiliser sa clé comme un index. Il est également possible d'obtenir une valeur true ou false selon que la clé que l'on cherche existe ou non dans le dictionnaire. On utilise pour cela le retour optionnel de la consultation

_, p := m["key2"]. L'usage du caractère
            '_' (underscore) va permettre de récupérer une valeur mais que nous signalons à Go comme inutilisée par la suite.
            Certaines constructions obligent ce subterfuge pour pouvoir fonctionner, comme c'est la cas dans la récupération
            de la présence ou non d'une valeur à l'index choisi.
         

Pour supprimer un couple key/value d'un dictionnaire, il suffit d'utiliser la référence à la clé dans l'instruction delete(mapname, key).

m := make(map[string]int)
m["key1"] = 4
m["key2"] = 2
fmt.Println(m) // affiche map[key1:4 key2:2]

delete(m, "key2")
fmt.Println(m) // affiche map[key1:4]

_, p := m["key1"]
fmt.Println(p) // affiche true car la clé existe dans le dictionnaire
_, p := m["key2"]
fmt.Println(p) // affiche false car la clé key2 n'existe pas dans le dictionnaire

h := map[string]int{"one":1, "two":2}
fmt.Println(h) // affiche map[two:2 one:1]

Les structures ou variables structurées


Par défaut, les tableaux contiennent un grand nombre de données mais toutes du même type. Pour obtenir un ensemble de données de types différents, il faut utiliser une structure. Pour déclarer une structure :

type nom_de_la_structure struct {
   nom_var_1 type
   nom_var_2 type
   ...
   nom_var_n type
}

Une fois la structure déclarée, elle peut être utilisée comme tout autre type dans Go

package main

import (
   "fmt"
)

func main() {
   type Personne struct {
      nom string
      prenom string
      age int
   }

   var perso1 Personne // utilisation du type structuré sans initialisation

   perso2 := Personne{"DOE", "John", 42} // Déclaration implicite avec affectation des valeurs par positionnement

   perso3 := Personne{prenom:"Jane", nom:"DOE", age:42} // Déclaration implicite avec affectation des valeurs par champ

   fmt.Println(perso2.nom) // affichage du nom
   perso3.nom = "DANE" // modification du nom
   fmt.Println(perso3.nom) // affichage du nom modifié
}

Lorsque la structure est définie avec les affectations de valeur dans les champs, TOUS les champs doivent avoir une valeur.


perso2 := Personne{"DOE", "John", 42} // Déclaration implicite avec affectation des valeurs par positionnement
// Tous les champs doivent être déclarés.
perso2bis := Personne{"DOE"} // Va générer une erreur car il manque l'initialisation de certains champs
La déclaration d'une structure à la façon clé:valeur est à mon sens la plus souple et la plus efficace sachant que, pour tout champ nom nommé, la valeur définie sera la valeur par défaut du type (0 pour int, {} pour une structure, ...).

perso3 := Personne {
   prenom:"Jane",
} // Déclaration implicite avec affectation des valeurs par champ
fmt.Printf("perso3 : %v\n", perso3) // Affichera {Jane "" 0}

Pour passer une structure en paramètre il suffit de faire comme pour tout type de donnée : utiliser une astérisque avant le type. La variable passée comme pointeur en paramètre s'utilise dans le corps de la fonction sans autre formalisme. A l'inverse du C qui impose une écriture fléchée (a->nom), en Go il n'y a pas de différence (a.nom). C'est le type du paramètre qui sera géré directement par le compilateur, pas par l'utilisateur.

package main

import (
   "fmt"
)

type Personne struct {
   nom string
   prenom string
}

func modif(p *Personne) {
   p.prenom = "Jane"
   p.nom = "DANE"
}

func main() {
   perso := Personne{"John", "DOE"}
   modif(&perso) // passage de la structure par paramètre
   fmt.Println(perso)
}

Parcourir les ensembles de données


Le parcours des données peut être simplifié en utilisant le mot-clé range. Celui-ci retourne à la fois l'index et la valeur de l'élément actuellement en cours d'itération.

package main

import (
   "fmt"
)

func main() {
   var jours = []string{"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"}

   // Récupère l'index (commence à 0 dans les ensembles tableaux et slices) et la valeur
   for i, jour := range jours {
      fmt.Println(jour, "est le juour numéro", (i+1), "de la semaine")
   }
}

Saisir des données


La saisie de données interactive peut s'effectuer par l'intermédiaire de la fonction Scan() présente dans le package bufio. Elle requiert la création d'un scanner mis en place par NewScanner() en mettant en paramètre le flux de saisie. Cela sera généralement os.Stdin.

La valeur saisie sera récupérée par la fonction Text(), qui permettra éventuellement de convertir le texte en valeur numérique en utilisant le package strconv et les fonctions similaires à celles fournies par "C" à savoir Atoi().

package main

import (
   "bufio"
   "fmt"
   "os"
)

func main() {
   scanner := bufio.NewScanner(os.Stdin) // capture sur l'entrée utilisateur
   fmt.Print("Que voulez-vous dire : ")
   scanner.Scan()
   inp := scanner.Text() // stockage dans une variable
   fmt.Println(inp)
   nbr, err := strconv.Atoi(scanner.Text()) // conversion en int
   if err != nil {
      fmt.Println("Ce n'est pas une valeur numérique...")
      os.Exit(-1);
   }
   fmr.Println(nbr)
}

Travailler avec JSON


Json est un langage très utilisé dans l'échange de données entre applications et permet notamment une très grande souplesse dans l'interaction avec les services WEB. Json présente un format de données proche d'une structure GO ou C et d'un dictionnaire de données. L'encodage et le décodage de Json s'effectue par l'intermédiaire de la bibliothèque encoding/json.

Le meilleur moyen de communiquer avec Json est par l'intermédiaire d'une structure

type Utilisateur struct {
   Nom string `json:"nom"`,
   Prenom string `json:"prenom"`,
   Age int `json:"age"`
}

Le fait de passer d'une structure Go à une structure Json se nomme le marshalling et le passage d'une structure Json à une structure Go se nomme unmarshalling. Les fonctions correspondantes sont les suivantes :

package main

import (
   "fmt"
   "os"
   "encoding/json"
)

type Utilisateur struct {
   Nom string `json:"nom"`,
   Prenom string `json:"prenom"`,
   Age int `json:"age"`
}

func main() {
   // Lecture d'un fichier au format Json
   dat, err := os.ReadFile("fichier.json")
   if err != nil {
      fmt.Println(err)
   } else {
      u := Utilisateur{}
      err = json.Unmarshal(dat, &u)
      if err != nil {
         fmt.Println(err)
      } else {
         fmt.Println("Nom : ", u.Nom)
         fmt.Println("Age : ", u.Age)
      }
   }


   // Ecriture d'un fichier au format Json
   u := Utilisateur{
      Nom: "DOE",
      Prenom: "John", 
      Age: 42
   }
   dat, err := json.Marshal(u)
   if err != nil {
      fmt.Println(err)
   } else {
      err = io.WriteFile("fichier.json", dat, 0644)
      if err != nil {
         fmt.Println(err)
      }
   }
}

Dans le cas où la structure du fichier Json n'est pas vraiment connue, il est possible de lire le contenu du Json dans un dictionnaire du type map[string]interface{} avec une clé au format string et un type structure.

dat := []byte(`{
   "first_name": "lane",
   "age": 30
}`)
m := map[string]interface{}{}
json.Unmarshal(dat, &m)
for k, v := range m {
   fmt.Printf("key: %v, value: %v\n", k, v)
}

// prints
// key: first_name, value: lane
// key: age, value: 30

Faire des tests unitaires en Go


Pour mettre en place une automatisation de tests unitaires, il faut essentiellement cinq éléments :

  1. Un fichier dans lequel vous allez mettre les tests à réaliser en nommant ce fichier comme celui contenant les fonctions à tester en suffixant par _test
  2. Importer le package testing dans ce fichier de tests
  3. Créer des fonctions de tests qui sont nommées Test(t *testing.T)
  4. Dans la fonction il faut faire appel à t.Error ou t.Failure pour indiquer que le test a échoué
  5. Pour exécuter les tests, il suffit de lancer go test dans le dossier du projet pour que Go exécute tous les tests qu'il pourra trouver