Les Bases (Go Tour 1/4)
Hello World
Le fameux Hello World en Go peut s’écrire de la manière suivante
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
Vous pouvez tester ce programme dans l’environnement local que vous avez installé, ou alors dans le Go Playground ou le Go better Playground.
Utilisation de l’environnement local
Pour développer du code Go dans l’environnement local, commencez par créer un dossier dans lequel vous souhaitez travailler et entrez dans ce dossier :
cd ~/projects
mkdir hello-world
cd hello-world
Initialisez ensuite un module Go :
go mod init hello-world
vous devriez obtenir un fichier go.mod avec le contenu suivant :
module hello-world
go 1.25.1
Note
Dans la pratique, le nom du module fait référence à un dépot git
(par exemple github.com/heia-fr/mymodule), mais pour les
exercices, vous pouvez utiliser un identifiant simple.
Editez votre fichier (par convention, le fichier qui contient
la méthode main est le fichier main.go).
Exécutez votre programme avec la commande suivante
go run .
Si votre package n’est composé que d’un seul fichier, vous pouvez aussi exécuter votre programme avec
go run main.go
Pour construire un exécutable, utilisez la commande suivante :
go build .
Vous obtiendrez alors un exécutable avec le même nom que vous avez
utilisé avec la commande go mod init. Si vous souhaitez que le
compilateur génère un autre fichier, vous pouvez spécifier son
nom avec l’option -o :
go build -o foo-bar .
Packages
Tous les programmes en Go sont composés de package. Lorsqu’on
exécute un fichier Go, on exécute la méthode main du package
main.
Les bibliothèques sont importées avec le mot clé import. Dans
l’exemple ci-dessus, on importe la bibliothèque fmt.
Si l’on importe plusieurs bibliothèques, on peut utiliser plusieurs
import :
import "fmt"
import "math"
ou l’on peut factoriser le import:
import (
"fmt"
"math"
)
Fonctions publiques/privées
Go ne possède pas de mot clé spécifique pour indiquer qu’une méthode est publique ou privée. C’est simplement la première lettre de l’identifiant qui est utilisé. Si un identifiant commence par une majuscule, il est public (exporté). Sinon, il est privé et n’est pas visible en dehors du package où il est déclaré.
Fonctions
En Go, les fonctions sont introduites avec le mot clé func :
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
Notez que contrairement à C ou Java, le type vient après le nom du paramètre.
Cette syntaxe était déjà utilisée à l’époque du Pascal et est à nouveau le standard
des langages modernes tels que Kotlin, Rust ou Swift. Notez aussi que Go
n’a pas besoin de ; pour indiquer la fin d’une instruction.
Si deux paramètres sont de même type, on peut simplifier l’écriture :
func add(x, y int) int {
return x + y
}
Une fonction en Go peut retourner plusieurs résultats :
func swap(x, y string) (string, string) {
return y, x
}
Cette technique est souvent utilisée pour indiquer si tout s’est bien passé ou s’il y a eu une erreur :
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
Variables
Les variables sont déclarées avec le mot clé var :
var x, y int
On peut aussi définir les valeurs initiales :
var x, y int = 1, 2
Go peut inférer le type des variables :
var i, j = 1, 2
À l’intérieur d’une fonction, une variable locale peut aussi
être déclarée et assignée avec l’opérateur := :
func main() {
k := 3
}
Types de base
Les principaux types de base en Go sont les suivants :
boolstringint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64byte(alias pouruint8)rune(alias pourint32et représente un caractère Unicode)float32,float64complex64,complex128
Conversions de types
L’expression T(v) convertit la valeur v dans le type T :
i := 42
f := float64(i)
u := uint(f)
Contrairement à C ou Java, Go ne fait pas de conversion implicite de types :
var a int32 = 1
var b int8 = 2
var c int32 = a + b // invalid operation: a + b
// (mismatched types int32 and int8)
Constantes
Les constantes sont déclarées avec le mot clé const :
const Pi = 3.14
Comme pour import, on peut factoriser le mot clé const :
const (
Pi = 3.14
Answer = 42
)
Les constantes numériques sans types sont de type int ou float64. Si
vous avez besoin de constantes avec d’autres types, vous pouvez spécifier ces derniers :
const (
s int8 = 12
pi float32 = 3.14
)
Boucles
En Go, il n’y a qu’une seule instruction pour les boucles : for. Il
n’y a pas de while ou de do/while.
for i := 0; i < 10; i++ {
sum += i
}
for sum < 1000 {
sum += sum
}
Instructions conditionnelles
L’instruction conditionnelle de base en Go est, comme la plupart
des autres langages, le if. Tout comme pour le for, le if
n’a pas besoin de parenthèses :
if x < 1 {
return sqrt(-x) + "i"
}
Un if peut commencer par une courte instruction avant la condition.
Cette construction est souvent utilisée pour traiter les erreurs :
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
Notez que dans l’exemple ci-dessus, la variable err n’est pas visible
en dehors du if
Comme la plupart des autres langages, Go implémente le mot clé else
pour définir l’action en cas de condition fausse.
En plus du if/else, Go propose aussi l’instruction switch :
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
Contrairement à C, C++ ou Java (mais comme Kotlin avec le when ou Rust avec le match),
le switch de Go n’exécute que le case correspondant. Vous n’avez donc pas besoin de mettre un
break à la fin des case.
De plus, les case ne doivent pas forcément être des constantes ou des entiers.
Si on ne définit pas d’expression après le switch, Go considère que l’expression est true
et permet ainsi de coder proprement une chaîne de if/else if/... :
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
Le switch n’exécute que le premier case qui correspond.
Defer
L’instruction defer permet de différer l’exécution de code.
Cette construction est souvent utilisée pour fermer des fichiers
ou autres ressources :
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
...
}
Pointeurs
Tout comme C ou C++, Go permet l’utilisation de pointeurs (mais contrairement à C++, Go ne propose pas de faire la différence entre un pointeur et une référence).
En Go, le type *T est un pointeur vers T :
var p *int
L’opérateur & permet d’obtenir l’adresse (donc la valeur du pointeur) d’une variable :
i := 42
p = &i
L’opérateur * déréférence un pointeur :
fmt.Println(*p)
*p = 21
Contrairement à C ou C++, Go ne permet de faire des opérations arithmétiques avec les pointeurs (et c’est une bonne chose).
Le null pointer en Go est défini par le mot clé nil.
Structures
Comme en C/C++ ou en Java avec les classes, Go permet de définir un
nouveau type permettant de regrouper des attributs avec le mot clé struct.
type Point struct {
X int
Y int
}
L’utilisation est la même qu’en C/C++ :
p := Point{1, 2}
p.X = 4
Notez que les attributs publics doivent commencer par des majuscules.
Les struct peuvent aussi s’utiliser avec les pointeurs :
p := Point{1, 2}
r := &p
r.X = 1e9
Notez que contrairement à C/C++, vous n’avez pas besoin de syntaxe
spéciale du genre x->X ou (*r).x pour accéder aux attributs. Go
sait que c’est un pointeur vers un struct et le déréférence
automatiquement pour vous.
On peut initialiser un struct de différentes manières. En spécifiant
tous les attributs :
p1 = Point{1, 2}
en nommant les attributs (dans l’exemple suivant, l’attribut Y vaut
implicitement zéro) :
p2 = Point{X: 1}
en initialisant le tout à zéro :
p3 = Point{}
on peut aussi déclarer un pointeur vers une structure :
r = &Point{1, 2}
Go est sufisamment intelligent pour savoir que si la variable ‘r’ est retournée par la fonction, il faut allouer la mémoire sur le tas (heap). Par contre, si ‘r’ est une variable locale qui n’est pas retournée, Go alloue la mémoire sur la pile (stack).
Il n’est donc pas nécessaire d’utiliser le mot clé new pour créer des objets en Go.
Il suffit d’utiliser la syntaxe &Type{...}.
Tableaux
On déclare un tableau (array) avec la syntaxe suivante :
var a [10]int
Mais en Go, on utilisera plutôt des slice que des tableaux. Le type
[]T (avec rien entre les crochets) indique un slice de T.
Un slice peut être construit par rapport à un tableau (ou un autre slice)
avec l’opération a[low : high] :
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
primes est un tableau de 6 éléments et s est un slice de 3 éléments avec
les valeurs primes[1], primes[2] et primes[3].
Les slices ne stockent pas de données eux-même mais ne sont que des références vers des tableaux. Pour plus de détails concernant les slices et leur implémentation, consultez la documentation.
On peut créer un slice avec un tableau associé avec la syntaxe suivante :
primes := []int{2, 3, 5, 7, 11, 13}
Contrairement à l’exemple précédent, on n’a pas spécifié de taille entre les crochets et
primes est maintenant un slice et non un array.
La capacité d’un slice c’est le nombre maximum d’éléments qu’il peut contenir en considérant l’index minimal et la taille du tableau associé. La longueur d’un slice c’est le nombre d’éléments entre l’index minimal et l’index maximal.
La longueur d’un slice est donnée par l’instruction len(s) et la capacité avec
l’instruction cap(s).
On peut créer un slice dynamiquement avec l’instruction make.
L’instruction suivante crée un slice de capacité 5 et de longueur 5. Les éléments du tableau associé sont initialisés à zéro :
a := make([]int, 5)
L’instruction suivante crée un slice de capacité 5 mais de longueur 3.
b := make([]int, 3, 5)
On peut refaire un slice avec la longueur égale à sa capacité avec :
b = b[:cap(b)]
Un slice peut contenir n’import quel type, y compris des autres slices
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
On peut ajouter des éléments à la fin d’un slice avec l’instruction append :
var s []int
s = append(s, 0)
s = append(s, 1)
s = append(s, 2, 3, 4)
Notez que append ne modifie pas le slice passé en argument, mais il en retourne
un nouveau. Si la capacité du tableau associé ne permet pas d’ajouter les
éléments, Go créra un tableau plus grand et y copiera les éléments du
tableau original.
Range
L’instruction range permet d’itérer sur tous les éléments d’un slice (ou d’une map).
Quand on itère sur un slice, on obtient l’index de chaque élément ainsi qu’une copie
de l’élément (un peu comme l’instruction enumerate de Python) :
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
Si l’indice ou la copie de la valeur n’est pas utilisé, il peut être assigné à _ :
for i, _ := range pow
for _, value := range pow
Si on a besoin que de l’indice, on peut aussi écrire :
for i := range pow
Maps
Une map est un tableau associatif (comme les dict en Python). Il permet d’associer
une valeur à une clé donnée.
type Coordinate struct {
Lat, Long float64
}
var m map[string]Coordinate
func main() {
m = make(map[string]Coordinate)
m["HEIA-FR"] = Coordinate{
46.7926, 7.15993,
}
fmt.Println(m["HEIA-FR"])
}
L’instruction make permet d’initialiser une map.
On peut aussi initialiser une map avec des constantes :
var m = map[string]Coordinate{
"HEIA-FR": Coordinate{
46.7926, 7.15993,
},
"Google": Coordinate{
37.42202, -122.08408,
},
}
ou plus simplement :
var m = map[string]Coordinate{
"HEIA-FR": {46.7926, 7.15993},
"Google": {37.42202, -122.08408},
}
Pour accéder à la valeur correspondant à une clé donnée, la syntaxe est la même que pour les tableaux ou les slices :
elem = m[key]
Il en va de même pour les modifications d’un élément :
m[key] = elem
Pour supprimer un élément, utilisez l’instruction delete :
delete(m, key)
Contrairement aux dict de Python, si on accède à un élément qui n’existe pas dans la map, Go retourne
simplement zéro. Pour faire la différence entre la valeur zéro et un élément qui n’existe pas, on utilise
la syntaxe suivante :
elem, ok = m[key]
ok est un booléen qui indique si la clé key se trouve dans la map.
Si on veut juste savoir si un élément existe sans y accéder, on peut faire :
_, ok = m[key]
Valeurs fonctions
En Go, les fonctions sont aussi des valeurs qui peuvent être assignées à des variables. C’est un peu comme les pointeurs de fonctions en C/C++ et ça permet d’imbriquer des fonctions dans des fonctions :
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
}
On peut aussi passer des fonctions comme paramètre à une autre fonction. Par exemple :
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
La fonction compute retourne un float64 et prend une fonction fn en argument, qui elle-même prend
deux arguments de type float64 et retourne également un float64. compute évalue la fonction fn
avec les arguments 3 et 4 et retourne le résultat de l’appel de fn.
Une fonction peut aussi retourner une fonction :
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
La fonction adder ci-dessus retourne une fonction qui prend un argument de type int
et qui retourne un autre int qui est la somme des arguments de tous les appels
précédents. La variable sum reste active même quand adder aura retourné à son appelant
et ne sera visible que par la fonction retournée.
Si on appelle plusieurs fois adder, toutes les fonctions retournées auront leur propre
variable sum.
Cette technique s’appelle Function closure.