Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

Go (Golang) : maîtrise les fondamentaux du langage de Google

Guide complet pour apprendre Go de zéro : syntaxe, goroutines, channels, et concepts essentiels pour développer des applications backend performantes

M
Mpia
12/15/2025 41 min
63 vues
1 comme.
#Code#Go#Backend
Go (Golang) : maîtrise les fondamentaux du langage de Google

Salut ! Aujourd'hui, on va explorer Go (aussi appelé Golang), le langage créé par Google qui a révolutionné le développement backend. Si tu as déjà galéré avec la complexité de C++, la lenteur de Python, ou les dépendances cauchemardesques de Node.js, Go va te faire respirer un grand bol d'air frais.

Go, c'est le langage qui allie la simplicité de Python, la performance de C, et la facilité de déploiement d'un binaire statique. Pas de JVM, pas d'interpréteur, pas de dépendances à installer - juste un exécutable qui marche partout.

Dans cet article, je vais t'expliquer pourquoi Go est devenu si populaire, et te guider à travers tous ses concepts fondamentaux. À la fin, tu seras capable de créer des applications Go performantes et concurrentes.

Prêt à découvrir Go ? Let's go ! 🚀

Pourquoi Go ?

Avant de plonger dans le code, comprenons pourquoi Go a été créé et pourquoi il connaît un tel succès.

L'histoire de Go

Go a été créé en 2009 chez Google par Robert Griesemer, Rob Pike et Ken Thompson (oui, LE Ken Thompson, co-créateur d'Unix !). Ils en avaient marre des temps de compilation interminables en C++ et voulaient un langage :

  • Simple : Peu de concepts à apprendre

  • Rapide : Compilation ultra-rapide, exécution performante

  • Moderne : Support natif de la concurrence

  • Productif : Déploiement facile, outillage excellent

Pourquoi les développeurs adorent Go

Performance proche du C : Go compile en code machine natif. Pas d'interpréteur, pas de VM. Le résultat ? Des applications ultra-rapides.

Compilation ultra-rapide : Même pour de gros projets, la compilation prend des secondes, pas des minutes. C'est révolutionnaire comparé à C++ ou Java.

Concurrence intégrée : Les goroutines et channels rendent la programmation concurrente aussi simple que du code séquentiel. C'est le super-pouvoir de Go.

Binaire statique : Go compile tout en un seul exécutable sans dépendances. Tu copies le fichier, tu l'exécutes, ça marche. Point final.

Simplicité : Go a volontairement peu de fonctionnalités. Pas d'héritage, pas de génériques (jusqu'à Go 1.18), pas de surcharge de fonctions. Moins de façons de faire = code plus uniforme.

Outillage intégré : go fmt (formatage automatique), go test (tests), go mod (gestion de dépendances) - tout est inclus.

Qui utilise Go ?

Des géants tech ont adopté Go massivement :

  • Uber : Backend de géolocalisation

  • Netflix : Services de streaming

  • Twitch : Chat et infrastructure

  • Dropbox : Migration de Python vers Go pour performance

Si c'est assez bon pour eux, c'est probablement assez bon pour toi !

Installation et premier programme

Commençons par installer Go et créer notre premier programme.

Installation

# macOS (avec Homebrew)
brew install go

# Linux
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# Windows : télécharger l'installeur sur go.dev

# Vérifier l'installation
go version

Hello World

Créons le programme Go traditionnel :

package main

import "fmt"

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

Sauvegarde ce fichier en hello.go et exécute :

go run hello.go
# Output: Hello, Go!

Que se passe-t-il ici ?

  • package main : Chaque fichier Go appartient à un package. main est spécial - c'est le point d'entrée.

  • import "fmt" : Importe le package fmt (format) pour l'affichage.

  • func main() : La fonction principale, exécutée au démarrage.

  • fmt.Println() : Affiche du texte avec un retour à la ligne.

Compiler en binaire

go build hello.go
# Crée un exécutable 'hello'

./hello
# Hello, Go!

Le binaire créé est autonome - tu peux le copier sur n'importe quelle machine (même architecture) et l'exécuter sans rien installer.

Variables et types

Go est un langage typé statiquement - chaque variable a un type défini à la compilation.

Déclaration de variables

Go offre plusieurs façons de déclarer des variables :

// Déclaration explicite
var nom string
nom = "Alice"

// Déclaration avec initialisation
var age int = 25

// Type inféré
var ville = "Paris" // Go devine que c'est un string

// Syntaxe courte (seulement dans les fonctions)
prenom := "Bob" // := déclare ET assigne
score := 100

La syntaxe := est la plus utilisée en Go. Elle déclare la variable et infère son type automatiquement. C'est concis et idiomatique.

Important : := fonctionne seulement dans les fonctions. Au niveau du package, tu dois utiliser var.

Types de base

Go a des types simples et explicites :

// Entiers
var a int = 42        // Taille dépend de l'architecture (32 ou 64 bits)
var b int8 = 127      // -128 à 127
var c int16 = 32767   // -32768 à 32767
var d int32 = 2147483647
var e int64 = 9223372036854775807

// Entiers non signés
var f uint = 42       // Seulement positifs
var g uint8 = 255     // 0 à 255
var h byte = 255      // Alias pour uint8

// Flottants
var prix float32 = 19.99
var pi float64 = 3.14159265359 // Plus précis, préféré

// Booléens
var estVrai bool = true
var estFaux bool = false

// Strings
var message string = "Bonjour"
var multiline = `Ceci est
un texte
multi-lignes` // Backticks pour multi-lignes

// Runes (caractères Unicode)
var lettre rune = 'A' // Alias pour int32

Valeurs par défaut (zero values)

En Go, les variables déclarées sans valeur initiale ont une "zero value" :

var nombre int       // 0
var texte string     // "" (string vide)
var actif bool       // false
var pointeur *int    // nil

C'est différent de languages comme JavaScript où une variable non initialisée est undefined. En Go, tout a toujours une valeur valide.

Constantes

Les constantes sont déclarées avec const et ne peuvent pas changer :

const PI = 3.14159
const NOM_APP = "MonApp"

// Constantes groupées
const (
    StatusOK = 200
    StatusNotFound = 404
    StatusError = 500
)

Structures de contrôle

if/else : Conditions

Go a une syntaxe if classique mais avec quelques spécificités.

age := 18

if age >= 18 {
    fmt.Println("Majeur")
} else {
    fmt.Println("Mineur")
}

// if avec initialisation
if score := getScore(); score > 90 {
    fmt.Println("Excellent!")
} // score n'existe que dans le bloc if

Pas de parenthèses : Contrairement à C/Java, Go n'utilise pas de parenthèses autour de la condition. Les accolades {} sont obligatoires, même pour une seule ligne.

for : La seule boucle de Go

Go n'a qu'une seule construction de boucle : for. Mais elle peut s'utiliser de plusieurs façons.

// for classique (comme C)
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// for comme while
compteur := 0
for compteur < 10 {
    fmt.Println(compteur)
    compteur++
}

// Boucle infinie
for {
    // Tourne indéfiniment
    // Utilise break pour sortir
}

// Itération sur un slice/array
nombres := []int{1, 2, 3, 4, 5}
for index, valeur := range nombres {
    fmt.Printf("Index: %d, Valeur: %d\n", index, valeur)
}

// Si on ne veut que les valeurs
for _, valeur := range nombres {
    fmt.Println(valeur)
}

Le mot-clé range est super pratique pour itérer sur des collections (slices, maps, strings).

switch : Multi-conditions

Le switch de Go est plus puissant que dans d'autres langages.

jour := "lundi"

switch jour {
case "lundi", "mardi", "mercredi", "jeudi", "vendredi":
    fmt.Println("Jour de travail")
case "samedi", "dimanche":
    fmt.Println("Weekend")
default:
    fmt.Println("Jour invalide")
}

// Switch sans expression (comme if/else)
score := 85

switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 70:
    fmt.Println("C")
default:
    fmt.Println("F")
}

Pas de fallthrough : Contrairement à C/Java, les cases en Go ne "tombent pas" automatiquement. Tu n'as pas besoin de break.

Fonctions

Les fonctions sont les blocs de construction fondamentaux en Go.

Syntaxe de base

// Fonction simple
func direBonjour(nom string) {
    fmt.Println("Bonjour", nom)
}

// Fonction avec retour
func additionner(a int, b int) int {
    return a + b
}

// Paramètres du même type (syntaxe courte)
func multiplier(a, b int) int {
    return a * b
}

// Appel
direBonjour("Alice")
resultat := additionner(5, 3)

Le type vient après : Contrairement à C/Java où tu écris int a, en Go c'est a int. Ça peut paraître bizarre au début, mais c'est plus lisible pour les types complexes.

Retours multiples

Go permet de retourner plusieurs valeurs - c'est très courant et idiomatique.

// Fonction retournant deux valeurs
func diviser(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division par zéro")
    }
    return a / b, nil
}

// Utilisation
resultat, err := diviser(10, 2)
if err != nil {
    fmt.Println("Erreur:", err)
} else {
    fmt.Println("Résultat:", resultat)
}

C'est le pattern standard en Go pour la gestion d'erreurs. Pas d'exceptions - les erreurs sont des valeurs retournées.

Fonctions variadiques

Les fonctions variadiques acceptent un nombre variable d'arguments.

func somme(nombres ...int) int {
    total := 0
    for _, n := range nombres {
        total += n
    }
    return total
}

// Utilisation
fmt.Println(somme(1, 2, 3))           // 6
fmt.Println(somme(1, 2, 3, 4, 5))     // 15

Fonctions anonymes et closures

// Fonction anonyme
func main() {
    saluer := func(nom string) {
        fmt.Println("Salut", nom)
    }

    saluer("Bob")

    // Closure : capture les variables externes
    compteur := 0
    incrementer := func() int {
        compteur++
        return compteur
    }

    fmt.Println(incrementer()) // 1
    fmt.Println(incrementer()) // 2
}

Arrays et Slices

Go a deux types de collections similaires mais différents : arrays et slices.

Arrays : Taille fixe

Les arrays ont une taille fixe définie à la compilation.

// Déclaration
var nombres [5]int // Array de 5 ints, initialisé à [0, 0, 0, 0, 0]

// Avec valeurs
fruits := [3]string{"Pomme", "Banane", "Orange"}

// Taille automatique
couleurs := [...]string{"Rouge", "Vert", "Bleu"} // ... calcule la taille

// Accès
fmt.Println(fruits[0]) // Pomme
fruits[1] = "Fraise"

Important : La taille fait partie du type. [3]int et [5]int sont des types différents.

Slices : Arrays dynamiques

Les slices sont comme des arrays mais avec une taille variable. C'est ce que tu utiliseras 99% du temps.

// Créer un slice
nombres := []int{1, 2, 3, 4, 5}

// Slice vide
var vide []int // nil slice

// make pour créer un slice avec capacité
s := make([]int, 5)      // longueur 5, valeurs = [0, 0, 0, 0, 0]
s2 := make([]int, 5, 10) // longueur 5, capacité 10

// Ajouter des éléments
nombres = append(nombres, 6)
nombres = append(nombres, 7, 8, 9)

// Longueur et capacité
fmt.Println(len(nombres)) // Nombre d'éléments
fmt.Println(cap(nombres)) // Capacité totale

// Slice d'un slice
sous := nombres[1:4] // Elements index 1, 2, 3 (4 exclu)
fmt.Println(sous)     // [2, 3, 4]

Différence clé : Les slices sont des références. Modifier un slice peut affecter d'autres slices qui partagent le même array sous-jacent.

Maps : Dictionnaires

Les maps sont des tables de hachage, comme les dictionnaires Python ou objets JavaScript.

// Créer une map
ages := map[string]int{
    "Alice": 25,
    "Bob":   30,
}

// Map vide
scores := make(map[string]int)

// Ajouter/Modifier
ages["Charlie"] = 35
scores["Niveau1"] = 100

// Accéder
age := ages["Alice"]
fmt.Println(age) // 25

// Vérifier l'existence
age, existe := ages["David"]
if existe {
    fmt.Println("Age de David:", age)
} else {
    fmt.Println("David n'existe pas")
}

// Supprimer
delete(ages, "Bob")

// Itérer
for nom, age := range ages {
    fmt.Printf("%s a %d ans\n", nom, age)
}

Attention : L'ordre d'itération des maps est aléatoire en Go. Ne compte jamais sur un ordre spécifique.

Structs : Types personnalisés

Les structs permettent de créer tes propres types composés.

// Définir une struct
type Personne struct {
    Nom    string
    Age    int
    Email  string
}

// Créer une instance
alice := Personne{
    Nom:   "Alice",
    Age:   25,
    Email: "alice@example.com",
}

// Notation courte (ordre des champs)
bob := Personne{"Bob", 30, "bob@example.com"}

// Accéder aux champs
fmt.Println(alice.Nom)
alice.Age = 26

// Pointeur vers struct
p := &alice
p.Email = "newemail@example.com" // Pas besoin de (*p).Email

Méthodes sur structs

En Go, on attache des méthodes aux structs.

type Rectangle struct {
    Largeur  float64
    Hauteur  float64
}

// Méthode avec receiver
func (r Rectangle) Aire() float64 {
    return r.Largeur * r.Hauteur
}

// Méthode qui modifie (pointer receiver)
func (r *Rectangle) Agrandir(facteur float64) {
    r.Largeur *= facteur
    r.Hauteur *= facteur
}

// Utilisation
rect := Rectangle{Largeur: 10, Hauteur: 5}
fmt.Println(rect.Aire())    // 50
rect.Agrandir(2)
fmt.Println(rect.Aire())    // 200

Receiver value vs pointer :

  • Value receiver (r Rectangle) : Reçoit une copie, ne peut pas modifier

  • Pointer receiver (r *Rectangle) : Reçoit une référence, peut modifier

Pointeurs : Références mémoire

Les pointeurs sont des adresses mémoire. Go a des pointeurs mais pas d'arithmétique de pointeurs (plus sûr que C).

// Déclarer un pointeur
var p *int

// & prend l'adresse
x := 42
p = &x

// * déréférence (accède à la valeur)
fmt.Println(*p) // 42

// Modifier via pointeur
*p = 100
fmt.Println(x) // 100 (x a changé)

// new alloue de la mémoire
p2 := new(int)
*p2 = 50

Pourquoi des pointeurs ?

  1. Éviter de copier de grandes structures

  2. Permettre aux fonctions de modifier des valeurs

  3. Représenter l'absence de valeur (nil)

Gestion d'erreurs

Go n'a pas d'exceptions. Les erreurs sont des valeurs retournées.

import (
    "errors"
    "fmt"
)

// Fonction qui peut échouer
func diviser(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division par zéro")
    }
    return a / b, nil
}

// Utilisation
resultat, err := diviser(10, 0)
if err != nil {
    fmt.Println("Erreur:", err)
    return
}
fmt.Println("Résultat:", resultat)

// Pattern idiomatique
if resultat, err := diviser(10, 2); err != nil {
    fmt.Println("Erreur:", err)
} else {
    fmt.Println("Résultat:", resultat)
}

Ce pattern peut paraître verbeux, mais il rend les erreurs explicites et force leur gestion.

Goroutines : Concurrence facile

Les goroutines sont des threads légers gérés par Go. C'est LE super-pouvoir de Go.

Lancer une goroutine

import (
    "fmt"
    "time"
)

func direBonjour(nom string) {
    for i := 0; i < 3; i++ {
        fmt.Println("Bonjour", nom)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Exécution séquentielle
    direBonjour("Alice")
    direBonjour("Bob")

    // Avec goroutines (concurrent)
    go direBonjour("Charlie") // Lance dans une goroutine
    go direBonjour("David")

    // Attendre un peu sinon main() termine avant les goroutines
    time.Sleep(1 * time.Second)
}

Le mot-clé go devant un appel de fonction lance cette fonction dans une nouvelle goroutine.

Attention : Si main() termine, toutes les goroutines sont arrêtées immédiatement.

Channels : Communication entre goroutines

Les channels permettent aux goroutines de communiquer en toute sécurité.

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d traite job %d\n", id, j)
        time.Sleep(time.Second)
        results <- j * 2 // Envoie le résultat
    }
}

func main() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)

    // Lancer 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Envoyer 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Collecter les résultats
    for r := 1; r <= 5; r++ {
        result := <-results
        fmt.Println("Résultat:", result)
    }
}

Les channels sont typés et peuvent être :

  • Unbuffered : make(chan int) - bloque jusqu'à ce qu'il y ait un receiver

  • Buffered : make(chan int, 10) - peut stocker 10 valeurs avant de bloquer

Interfaces : Polymorphisme

Les interfaces en Go sont implicites - pas besoin de déclarer qu'un type implémente une interface.

// Définir une interface
type Forme interface {
    Aire() float64
}

// Rectangle implémente Forme (implicitement)
type Rectangle struct {
    Largeur, Hauteur float64
}

func (r Rectangle) Aire() float64 {
    return r.Largeur * r.Hauteur
}

// Cercle implémente aussi Forme
type Cercle struct {
    Rayon float64
}

func (c Cercle) Aire() float64 {
    return 3.14159 * c.Rayon * c.Rayon
}

// Fonction qui accepte n'importe quelle Forme
func afficherAire(f Forme) {
    fmt.Printf("Aire: %.2f\n", f.Aire())
}

func main() {
    r := Rectangle{Largeur: 10, Hauteur: 5}
    c := Cercle{Rayon: 7}

    afficherAire(r) // Aire: 50.00
    afficherAire(c) // Aire: 153.94
}

Si un type a toutes les méthodes d'une interface, il implémente cette interface automatiquement.

Modules et packages

Depuis Go 1.11, Go utilise les modules pour gérer les dépendances.

Créer un module

# Créer un nouveau module
go mod init monapp

# Ajouter une dépendance
go get github.com/gorilla/mux

# Le fichier go.mod est créé automatiquement

Organiser le code en packages

// fichier: math/operations.go
package math

func Add(a, b int) int {
    return a + b
}

// fichier: main.go
package main

import (
    "fmt"
    "monapp/math" // Import du package local
)

func main() {
    result := math.Add(5, 3)
    fmt.Println(result)
}

Convention : Les noms exportés (publics) commencent par une majuscule. Les minuscules sont privés au package.

Exemple complet : Serveur HTTP

Créons un serveur web simple - un cas d'usage classique de Go.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Utilisateur struct {
    ID   int    `json:"id"`
    Nom  string `json:"nom"`
    Email string `json:"email"`
}

var utilisateurs = []Utilisateur{
    {ID: 1, Nom: "Alice", Email: "alice@example.com"},
    {ID: 2, Nom: "Bob", Email: "bob@example.com"},
}

func getUtilisateurs(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(utilisateurs)
}

func accueil(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Bienvenue sur l'API!")
}

func main() {
    http.HandleFunc("/", accueil)
    http.HandleFunc("/users", getUtilisateurs)

    fmt.Println("Serveur démarré sur :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Exécute avec go run main.go et visite http://localhost:8080/users !

Bonnes pratiques

1. Gestion d'erreurs explicite

// ✓ Bon : toujours vérifier les erreurs
file, err := os.Open("fichier.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

2. Utilise defer pour le cleanup

file, err := os.Open("data.txt")
if err != nil {
    return err
}
defer file.Close() // Sera exécuté à la fin de la fonction

// Utilise le fichier...

3. Nom des variables courts

// Go préfère les noms courts pour variables locales
for i := 0; i < 10; i++ { } // Pas index
if err != nil { }              // Pas error

4. gofmt pour formater

# Formate automatiquement ton code
go fmt ./...

5. Tests intégrés

// fichier: math.go
func Add(a, b int) int {
    return a + b
}

// fichier: math_test.go
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Exécute avec go test.

Conclusion

Go est un langage qui privilégie la simplicité et la clarté. Il n'a pas toutes les fonctionnalités des langages modernes, et c'est volontaire. Ce minimalisme te force à écrire du code simple et maintenable.

Points clés à retenir :

  • Simplicité : Peu de concepts, facile à apprendre

  • Performance : Compile en natif, rapide comme C

  • Concurrence : Goroutines et channels rendent le concurrent facile

  • Outillage : go fmt, go test, go mod - tout intégré

  • Déploiement : Un seul binaire statique

Concepts essentiels :

  • Types statiques avec inférence

  • Gestion d'erreurs explicite (pas d'exceptions)

  • Goroutines pour la concurrence

  • Interfaces implicites

  • Pointeurs sans arithmétique

Go brille particulièrement pour :

  • Services web et APIs

  • Outils CLI

  • Microservices

  • DevOps et infrastructure (Kubernetes, Docker, Terraform)

Maintenant, va créer quelque chose d'incroyable avec Go !


Ressources pour aller plus loin :

Tu as des questions sur Go ? Partage-les dans les commentaires !

Commentaires (1)

Laisser un commentaire