Aller au contenu

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 . 
Go peut aussi installer un binaire. Lisez la documentation pour plus d’info.

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 :

  • bool
  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • byte (alias pour uint8)
  • rune (alias pour int32 et représente un caractère Unicode)
  • float32, float64
  • complex64, 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.