Maîtrisez Go pour le Backend : Construisez des APIs Performantes et Scalables
Maîtrisez Go pour le Backend : Construisez des APIs Performantes et Scalables

Les Fondamentaux de Go : Types, Fonctions et Contrôle de Flux

Introduction

Bienvenue dans cette leçon dédiée aux fondamentaux de Go, un langage de plus en plus prisé pour le développement backend, les microservices et les systèmes distribués. Dans le cadre de notre cours "Maîtrisez Go pour le Backend : Construisez des APIs Performantes et Scalables", la compréhension des types, des fonctions et du contrôle de flux est essentielle. Ces concepts sont les piliers sur lesquels reposent toutes les applications Go, des plus simples aux plus complexes, et sont d'une importance capitale pour écrire du code propre, efficace et maintenable.

Nous explorerons comment Go gère les données avec ses systèmes de types, comment structurer votre logique avec des fonctions réutilisables, et comment diriger le flot d'exécution de votre programme avec les instructions conditionnelles et les boucles. À la fin de cette leçon, vous aurez une base solide pour commencer à écrire vos propres programmes Go.

1. Les Types en Go : Les Briques de Construction

En Go, comme dans tout langage fortement typé, les types définissent la nature des données que vous manipulez. Comprendre les types est fondamental pour éviter les erreurs, optimiser les performances et écrire du code lisible.

1.1 Qu'est-ce qu'un Type ?

Un type est une classification qui spécifie les opérations qui peuvent être effectuées sur une valeur, ainsi que la manière dont cette valeur est stockée en mémoire. Go est un langage statiquement typé, ce qui signifie que le type de chaque variable est connu à la compilation et ne change pas pendant l'exécution.

1.2 Types Primitifs Fondamentaux

Go offre un ensemble de types primitifs pour gérer les données courantes :

  • Nombres entiers :
    • int: Le type entier le plus courant, sa taille dépend de l'architecture du système (32 ou 64 bits).
    • int8, int16, int32, int64: Entiers signés de tailles spécifiques.
    • uint, uint8, uint16, uint32, uint64, uintptr: Entiers non signés (positifs ou zéro) de tailles spécifiques. uintptr est utilisé pour stocker des adresses mémoire.
    • byte: Alias pour uint8, souvent utilisé pour représenter des données binaires.
    • rune: Alias pour int32, utilisé pour représenter un caractère Unicode.
  • Nombres à virgule flottante :
    • float32: Nombres à virgule flottante de 32 bits.
    • float64: Nombres à virgule flottante de 64 bits (le plus couramment utilisé pour sa précision).
  • Booléens :
    • bool: Représente une valeur de vérité, soit true soit false.
  • Chaînes de caractères :
    • string: Une séquence immuable de caractères byte. Les chaînes en Go sont encodées en UTF-8.

1.3 Déclaration et Initialisation des Variables

En Go, il existe plusieurs façons de déclarer et d'initialiser des variables.

Le mot-clé var

C'est la méthode de déclaration traditionnelle, où vous spécifiez explicitement le type de la variable.

package main

import "fmt"

func main() {
    var nom string        // Déclaration sans initialisation
    var age int = 30      // Déclaration et initialisation
    var estActif bool = true

    nom = "Alice" // Assignation après déclaration

    fmt.Println("Nom:", nom, "Age:", age, "Actif:", estActif)

    // Déclaration de plusieurs variables du même type
    var x, y int
    x = 10
    y = 20
    fmt.Println("x:", x, "y:", y)

    // Déclaration de plusieurs variables de types différents
    var (
        prenom string = "Bob"
        taille float64 = 1.75
    )
    fmt.Println("Prénom:", prenom, "Taille:", taille)
}

Explication :

  • var nom string déclare une variable nom de type string. Si non initialisée, elle prend sa "zéro-valeur".
  • var age int = 30 déclare age de type int et l'initialise à 30.
  • Les blocs var (...) permettent de grouper des déclarations de variables pour une meilleure lisibilité.

L'opérateur de déclaration courte :=

C'est la manière la plus idiomatique et la plus courante de déclarer et d'initialiser des variables en Go, surtout à l'intérieur des fonctions. Go infère le type de la variable à partir de la valeur assignée.

package main

import "fmt"

func main() {
    message := "Bonjour Go!" // Go infère que 'message' est de type string
    nombre := 123            // Go infère que 'nombre' est de type int
    pi := 3.14               // Go infère que 'pi' est de type float64
    estValide := true        // Go infère que 'estValide' est de type bool

    fmt.Println(message, nombre, pi, estValide)

    // Cet opérateur ne peut être utilisé que pour la déclaration initiale.
    // Pour une réassignation, utilisez simplement '=' :
    nombre = 456
    fmt.Println("Nouveau nombre:", nombre)
}

Explication :

  • message := "Bonjour Go!" déclare message et l'initialise. Le compilateur Go détermine automatiquement que message est de type string.
  • L'opérateur := est une commodité qui combine déclaration et assignation. Il ne peut être utilisé que lorsque la variable est nouvellement déclarée dans la portée actuelle.

Les "zéro-valeurs"

Lorsqu'une variable est déclarée mais non initialisée explicitement, Go lui assigne une zéro-valeur par défaut, qui est la "valeur nulle" du type correspondant :

  • 0 pour les types numériques (int, float, etc.)
  • false pour les bool
  • "" (chaîne vide) pour les string
  • nil pour les pointeurs, fonctions, slices, maps, channels, et interfaces
package main

import "fmt"

func main() {
    var i int
    var f float64
    var b bool
    var s string

    fmt.Printf("Zéro-valeur de int: %v\n", i)    // 0
    fmt.Printf("Zéro-valeur de float64: %v\n", f) // 0
    fmt.Printf("Zéro-valeur de bool: %v\n", b)   // false
    fmt.Printf("Zéro-valeur de string: %v\n", s) // ""
}

1.4 Conversion de Types

Go est très strict sur les types. Les conversions implicites entre types différents (même s'ils semblent compatibles) ne sont pas autorisées. Vous devez effectuer des conversions explicites.

package main

import "fmt"

func main() {
    var entier int = 42
    var flottant float64 = float64(entier) // Convertit int en float64

    fmt.Printf("Entier: %d, Flottant: %f\n", entier, flottant)

    var chaine string = "123"
    // var nombre int = int(chaine) // ERREUR: Impossible de convertir directement string en int

    // Pour convertir string en nombre, il faut utiliser des fonctions du package strconv
    import "strconv" // Ajouté pour l'exemple
    valeurNumerique, err := strconv.Atoi(chaine)
    if err != nil {
        fmt.Println("Erreur de conversion:", err)
    } else {
        fmt.Printf("Chaîne '%s' convertie en int: %d\n", chaine, valeurNumerique)
    }

    var asciiValue byte = 'A' // 'A' est un caractère, mais byte est uint8
    var intValue int = int(asciiValue)
    fmt.Printf("Caractère 'A' (byte): %d, Converti en int: %d\n", asciiValue, intValue)
}

Explication :

  • float64(entier) convertit la valeur de entier en float64.
  • La conversion de string vers un type numérique nécessite des fonctions spécifiques du package strconv, car ce n'est pas une simple réinterprétation des bits.

2. Les Fonctions en Go : Modularité et Réutilisabilité

Les fonctions sont le cœur de l'organisation logique de votre code en Go. Elles permettent de découper des problèmes complexes en tâches plus petites et réutilisables, améliorant ainsi la lisibilité, la maintenabilité et la testabilité du code.

2.1 Définition et Syntaxe de Base

Une fonction en Go est définie en utilisant le mot-clé func, suivi du nom de la fonction, d'une liste de paramètres entre parenthèses, et optionnellement d'un type de retour. Le corps de la fonction est délimité par des accolades {}.

package main

import "fmt"

// add est une fonction qui prend deux entiers et retourne leur somme.
func add(a int, b int) int {
    return a + b
}

// greet est une fonction qui prend un nom et ne retourne rien.
func greet(name string) {
    fmt.Println("Bonjour,", name, "!")
}

func main() {
    sum := add(5, 3)
    fmt.Println("La somme est:", sum)

    greet("Go Programmer")
}

Explication :

  • func add(a int, b int) int:
    • func: Mot-clé pour définir une fonction.
    • add: Nom de la fonction.
    • (a int, b int): Liste des paramètres. Chaque paramètre est suivi de son type.
    • int: Type de la valeur retournée par la fonction.
  • func greet(name string): Cette fonction ne spécifie pas de type de retour, ce qui signifie qu'elle ne retourne aucune valeur (un void implicite).

2.2 Paramètres et Valeurs de Retour

Paramètres avec types omis

Si plusieurs paramètres consécutifs sont du même type, vous pouvez omettre le type pour tous sauf le dernier :

func add(a, b int) int { // Plus court que add(a int, b int) int
    return a + b
}

Retours multiples : La gestion des erreurs en Go

Go se distingue par sa capacité à retourner plusieurs valeurs à partir d'une fonction. Cette fonctionnalité est cruciale, notamment pour la gestion des erreurs, où une fonction retourne souvent à la fois un résultat et une erreur.

package main

import (
	"errors"
	"fmt"
)

// diviser divise a par b. Elle retourne le résultat et une erreur si b est zéro.
func diviser(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("impossible de diviser par zéro")
    }
    return a / b, nil // nil signifie 'pas d'erreur'
}

func main() {
    result, err := diviser(10, 2)
    if err != nil {
        fmt.Println("Erreur:", err)
    } else {
        fmt.Println("Résultat de la division:", result)
    }

    result, err = diviser(10, 0)
    if err != nil {
        fmt.Println("Erreur:", err) // Affiche: Erreur: impossible de diviser par zéro
    } else {
        fmt.Println("Résultat de la division:", result)
    }
}

Explication :

  • func diviser(a, b float64) (float64, error): Indique que la fonction retourne un float64 et un type error.
  • return a / b, nil: En cas de succès, le résultat est retourné, et nil est retourné pour l'erreur, indiquant qu'aucune erreur ne s'est produite.
  • return 0, errors.New(...): En cas d'échec, 0 est retourné comme valeur par défaut et une nouvelle erreur est créée et retournée.
  • L'idiome if err != nil est très courant en Go pour vérifier les erreurs.

Fonctions variadiques (...)

Une fonction variadique peut accepter un nombre variable d'arguments du même type.

package main

import "fmt"

// sumAll calcule la somme de tous les entiers passés en argument.
func sumAll(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

func main() {
    fmt.Println(sumAll(1, 2, 3))        // Output: 6
    fmt.Println(sumAll(10, 20, 30, 40)) // Output: 100
    fmt.Println(sumAll())               // Output: 0
}

Explication :

  • numbers ...int: Le ... indique que numbers est un paramètre variadique, et il sera traité comme une slice d'entiers à l'intérieur de la fonction.

2.3 Portée des Variables (Scope)

La portée d'une variable détermine où elle peut être accédée et utilisée dans votre programme.

  • Les variables déclarées à l'intérieur d'une fonction ont une portée locale à cette fonction.
  • Les variables déclarées au niveau du package (en dehors de toute fonction) ont une portée globale au package et peuvent être exportées (rendues accessibles depuis d'autres packages) si leur nom commence par une majuscule.
package main

import "fmt"

var packageScopeVar string = "Je suis une variable au niveau du package." // Portée globale au package

func main() {
    localScopeVar := "Je suis une variable locale à main." // Portée locale à main()

    fmt.Println(packageScopeVar)
    fmt.Println(localScopeVar)

    anotherFunction()
    // fmt.Println(anotherLocalVar) // ERREUR: anotherLocalVar n'est pas définie ici
}

func anotherFunction() {
    anotherLocalVar := "Je suis une variable locale à anotherFunction."
    fmt.Println(packageScopeVar) // Accès possible à packageScopeVar
    fmt.Println(anotherLocalVar)
}

2.4 Fonctions Anonymes et Closures

Go permet de définir des fonctions sans nom, appelées fonctions anonymes. Elles sont souvent utilisées pour des opérations rapides, des fonctions passées en tant qu'arguments ou pour implémenter des closures. Une closure est une fonction anonyme qui "capture" des variables de l'environnement où elle a été définie, même après que cet environnement ait disparu.

package main

import "fmt"

func main() {
    // Fonction anonyme assignée à une variable
    addition := func(a, b int) int {
        return a + b
    }
    fmt.Println("Somme (fonction anonyme):", addition(10, 5))

    // Fonction anonyme exécutée immédiatement (IIFE - Immediately Invoked Function Expression)
    func() {
        fmt.Println("Je suis une fonction anonyme exécutée immédiatement!")
    }()

    // Closure: La fonction interne se souvient de 'base'
    createMultiplier := func(base int) func(int) int {
        return func(factor int) int {
            return base * factor // 'base' est capturé de l'environnement parent
        }
    }

    multiplyBy5 := createMultiplier(5)
    fmt.Println("5 * 4 =", multiplyBy5(4)) // Output: 20

    multiplyBy10 := createMultiplier(10)
    fmt.Println("10 * 3 =", multiplyBy10(3)) // Output: 30
}

Explication :

  • La fonction createMultiplier retourne une autre fonction anonyme. Cette fonction anonyme "ferme" (closure) sur la variable base de son environnement parent. Chaque appel à createMultiplier crée une nouvelle closure avec sa propre copie de base.

2.5 L'Instruction defer : Nettoyage Simplifié

L'instruction defer reporte l'exécution d'un appel de fonction jusqu'à ce que la fonction englobante retourne. Les appels defer sont empilés (LIFO - Last In, First Out). C'est extrêmement utile pour s'assurer que des ressources (fichiers ou connexions à la base de données) sont libérées, même en cas d'erreur.

package main

import "fmt"

func main() {
    fmt.Println("Début de main")

    // Les appels defer sont exécutés dans l'ordre inverse de leur définition
    defer fmt.Println("Troisième defer (dernière exécutée)")
    defer fmt.Println("Deuxième defer")
    defer fmt.Println("Premier defer (première exécutée)")

    fmt.Println("Milieu de main")

    // Exemple d'utilisation pratique : fermeture de fichier
    // Dans un vrai programme, on ouvrirait un fichier ici.
    simulateFileOperation()

    fmt.Println("Fin de main")
}

func simulateFileOperation() {
    fmt.Println("\nOuverture du fichier...")
    // Imaginez ici le code pour ouvrir un fichier
    defer fmt.Println("Fermeture du fichier.") // Ceci sera exécuté avant que simulateFileOperation ne retourne

    fmt.Println("Lecture/Écriture dans le fichier...")
    // Imaginez ici le code pour lire/écrire
    fmt.Println("Opération fichier terminée.")
}

Explication :

  • Les appels defer sont placés sur une pile. Quand la fonction main ou simulateFileOperation termine son exécution (normalement ou via un panic), les fonctions defer sont dépilées et exécutées. Cela garantit que le nettoyage est toujours effectué.

3. Le Contrôle de Flux en Go : Diriger la Logique

Le contrôle de flux vous permet de dicter l'ordre dans lequel les instructions de votre programme sont exécutées, en fonction de conditions ou pour répéter des actions.

3.1 Instructions Conditionnelles : if, else if, else

La structure if en Go est similaire à d'autres langages, mais sans parenthèses autour de la condition. Les accolades sont obligatoires.

package main

import "fmt"

func main() {
    temperature := 25

    if temperature > 30 {
        fmt.Println("Il fait très chaud!")
    } else if temperature > 20 { // else if est facultatif
        fmt.Println("Il fait bon.")
    } else { // else est facultatif
        fmt.Println("Il fait frais.")
    }

    // Initialisation courte dans `if`
    // `err` est scope-limitée à ce bloc if/else
    if result, err := diviser(10, 0); err != nil {
        fmt.Println("Erreur de division:", err)
    } else {
        fmt.Println("Résultat de la division:", result)
    }

    // `result` et `err` ne sont pas accessibles ici
    // fmt.Println(result) // Ceci provoquerait une erreur de compilation
}

// Fonction diviser réutilisée de l'exemple précédent
func diviser(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("impossible de diviser par zéro")
    }
    return a / b, nil
}

Explication :

  • La syntaxe if condition { ... } else if condition { ... } else { ... } est standard.
  • L'initialisation courte dans if (if result, err := diviser(10, 0); err != nil) est très idiomatique en Go, permettant de déclarer des variables et de vérifier une condition en une seule ligne. Les variables déclarées ainsi sont limitées à la portée du bloc if/else.

3.2 L'Instruction switch

L'instruction switch est une alternative propre et concise à de multiples if-else if pour tester plusieurs conditions basées sur une seule valeur ou expression. En Go, un switch exécute uniquement le premier cas qui correspond et ne "tombe" pas dans le cas suivant par défaut (fallthrough est désactivé implicitement).

package main

import "fmt"

func main() {
    jour := "Mardi"

    switch jour {
    case "Lundi":
        fmt.Println("C'est le début de la semaine.")
    case "Mardi", "Mercredi", "Jeudi": // Plusieurs valeurs dans un même case
        fmt.Println("C'est le milieu de la semaine.")
    case "Vendredi":
        fmt.Println("C'est presque le week-end!")
    default: // Cas par défaut si aucune correspondance n'est trouvée
        fmt.Println("C'est le week-end ou un jour inconnu.")
    }

    // switch sans condition (équivalent à des if/else if)
    heure := 15
    switch {
    case heure < 12:
        fmt.Println("Bonjour matin!")
    case heure < 18:
        fmt.Println("Bonne après-midi!")
    default:
        fmt.Println("Bonne soirée!")
    }

    // L'instruction `fallthrough` (rarement utilisée)
    // Force l'exécution du case suivant
    grade := "B"
    switch grade {
    case "A":
        fmt.Println("Excellent!")
    case "B":
        fmt.Println("Très bien!")
        fallthrough // Exécute aussi le case "C"
    case "C":
        fmt.Println("Bien.")
    default:
        fmt.Println("À travailler davantage.")
    }
    // Output:
    // Très bien!
    // Bien.
}

Explication :

  • Le switch en Go ne nécessite pas de break car il sort du bloc switch après la première correspondance trouvée.
  • Un switch sans expression agit comme une série de if-else if, où chaque case est une condition booléenne.
  • fallthrough doit être utilisé avec parcimonie car il rend le contrôle de flux moins évident.

3.3 Les Boucles : L'Unique for

Go n'a qu'une seule instruction de boucle : for. Elle est incroyablement flexible et peut être utilisée pour implémenter tous les types de boucles que vous trouveriez dans d'autres langages (boucle for classique, boucle while, boucle infinie, etc.).

Boucle classique (for avec initialisation, condition, post-instruction)

package main

import "fmt"

func main() {
    fmt.Println("Comptage de 0 à 4:")
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
}

Explication :

  • Similaire à la boucle for en C/Java. i := 0 (initialisation), i < 5 (condition), i++ (post-instruction exécutée à chaque itération).

Boucle "while-like" (for avec seule condition)

package main

import "fmt"

func main() {
    sum := 1
    fmt.Println("\nDoublons tant que la somme est inférieure à 100:")
    for sum < 100 {
        sum += sum // sum = sum * 2
        fmt.Println(sum)
    }
}

Explication :

  • L'équivalent d'une boucle while d'autres langages. La boucle continue tant que la condition sum < 100 est vraie.

Boucle infinie (for { })

package main

import "fmt"

func main() {
    fmt.Println("\nBoucle infinie (arrêt après 3 itérations):")
    counter := 0
    for { // Pas de condition, la boucle est infinie
        fmt.Println("Itération", counter)
        counter++
        if counter >= 3 {
            break // Utilisez `break` pour sortir de la boucle infinie
        }
    }
}

Explication :

  • Une boucle for {} s'exécute indéfiniment à moins qu'une instruction break ne soit rencontrée.

L'itération for-range

L'instruction for-range est utilisée pour itérer sur des collections comme les chaînes de caractères, les tableaux (arrays), les slices, les maps et les channels.

package main

import "fmt"

func main() {
    // Itérer sur une slice
    nombres := []int{10, 20, 30, 40}
    fmt.Println("\nItération sur une slice:")
    for index, value := range nombres {
        fmt.Printf("Index: %d, Valeur: %d\n", index, value)
    }

    // Itérer sur une chaîne (caractères Unicode)
    salutation := "Bonjour Monde!"
    fmt.Println("\nItération sur une chaîne:")
    for index, runeValue := range salutation {
        fmt.Printf("Index: %d, Caractère: %c (Unicode: %d)\n", index, runeValue, runeValue)
    }

    // Itérer sur une map
    fruits := map[string]string{"pomme": "rouge", "banane": "jaune"}
    fmt.Println("\nItération sur une map:")
    for cle, valeur := range fruits {
        fmt.Printf("Clé: %s, Valeur: %s\n", cle, valeur)
    }

    // Ignorer l'index/clé ou la valeur
    fmt.Println("\nItération sans index/clé:")
    for _, value := range nombres { // Utiliser '_' pour ignorer l'index
        fmt.Println("Valeur (sans index):", value)
    }
}

Explication :

  • for index, value := range collection: À chaque itération, index reçoit l'index (ou la clé pour les maps) et value reçoit l'élément.
  • Si vous n'avez besoin que de la valeur (ou de l'index), vous pouvez utiliser l'identifiant vide _ pour ignorer l'autre.

3.4 Instructions de Saut : break, continue, goto

Ces instructions modifient le flux normal d'exécution des boucles.

  • break: Termine l'exécution de la boucle la plus interne (ou du switch ou select) et le contrôle passe à l'instruction suivant la boucle.
  • continue: Saute le reste de l'itération actuelle de la boucle et passe à l'itération suivante.
  • goto: Transfère le contrôle du programme à une étiquette spécifiée. L'utilisation de goto est fortement déconseillée en Go (et dans la plupart des langages modernes) car elle rend le code difficile à lire et à maintenir.
package main

import "fmt"

func main() {
    fmt.Println("\nExemple de break et continue:")
    for i := 0; i < 10; i++ {
        if i%2 != 0 { // Si i est impair
            continue // Passe à l'itération suivante
        }
        fmt.Println(i) // Affiche uniquement les nombres pairs
        if i == 6 {
            break // Sort de la boucle lorsque i est 6
        }
    }
    // Output: 0, 2, 4, 6

    // Exemple de goto (à éviter!)
    fmt.Println("\nExemple de goto (à éviter!):")
    i := 0
Start: // Étiquette
    fmt.Println(i)
    i++
    if i < 3 {
        goto Start // Saute à l'étiquette Start
    }
    // Output: 0, 1, 2
}

Exemple Pratique Complet

Combinons tout ce que nous avons appris dans un exemple qui simule la validation d'une requête API pour la création d'un utilisateur.

package main

import (
	"errors"
	"fmt"
	"strconv" // Pour la conversion de string en int
	"strings" // Pour les opérations sur les chaînes
)

// UserRequest simule une structure de données pour une requête de création d'utilisateur
// En réalité, ce serait une struct. Pour cet exemple, nous utiliserons une map pour la simplicité,
// mais les types et les structs sont préférables pour les APIs Go.
type UserRequest map[string]string

// validateAndProcessUserRequest valide les champs d'une requête utilisateur
// et renvoie un message de succès ou une erreur.
func validateAndProcessUserRequest(req UserRequest) (string, error) {
    // 1. Validation des champs obligatoires
    // Utilisation de la boucle for-range et if
    requiredFields := []string{"name", "email", "age"}
    for _, field := range requiredFields {
        if _, ok := req[field]; !ok || strings.TrimSpace(req[field]) == "" {
            return "", fmt.Errorf("champ manquant ou vide: '%s'", field)
        }
    }

    // 2. Validation du format de l'e-mail (simplifiée)
    email := req["email"]
    if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
        return "", errors.New("format d'email invalide")
    }

    // 3. Validation de l'âge
    ageStr := req["age"]
    age, err := strconv.Atoi(ageStr) // Conversion de type string -> int
    if err != nil {
        return "", fmt.Errorf("l'âge doit être un nombre valide: %w", err)
    }
    if age < 18 || age > 120 {
        return "", errors.New("l'âge doit être entre 18 et 120 ans")
    }

    // 4. Génération d'un message de bienvenue conditionnel
    // Utilisation de switch et fonctions avec paramètres
    welcomeMessage := createWelcomeMessage(req["name"], age)

    // Simuler un enregistrement en base de données ou autre opération backend
    // Utilisation de defer pour simuler la fermeture d'une ressource
    fmt.Println("Ouverture de la connexion à la base de données...")
    defer fmt.Println("Fermeture de la connexion à la base de données.") // S'exécutera à la fin de la fonction

    fmt.Printf("Utilisateur '%s' avec email '%s' et âge %d validé.\n", req["name"], email, age)

    return welcomeMessage, nil
}

// createWelcomeMessage génère un message de bienvenue basé sur le nom et l'âge.
func createWelcomeMessage(name string, age int) string {
    switch { // switch sans condition
    case age < 30:
        return fmt.Sprintf("Bienvenue, %s ! Belle jeunesse !", name)
    case age < 60:
        return fmt.Sprintf("Ravi de vous voir, %s ! Votre expérience est précieuse.", name)
    default:
        return fmt.Sprintf("Bonjour, %s ! C'est un honneur de vous accueillir.", name)
    }
}

func main() {
    fmt.Println("--- Test 1: Requête valide ---")
    validReq := UserRequest{
        "name":  "Jean Dupont",
        "email": "jean.dupont@example.com",
        "age":   "35",
    }
    msg, err := validateAndProcessUserRequest(validReq)
    if err != nil {
        fmt.Println("Erreur lors de la validation:", err)
    } else {
        fmt.Println("Succès:", msg)
    }

    fmt.Println("\n--- Test 2: Champ manquant ---")
    missingFieldReq := UserRequest{
        "name":  "Marie Curie",
        "email": "marie.curie@example.com",
        // "age" est manquant
    }
    msg, err = validateAndProcessUserRequest(missingFieldReq)
    if err != nil {
        fmt.Println("Erreur lors de la validation:", err) // Attendu: champ manquant ou vide: 'age'
    } else {
        fmt.Println("Succès:", msg)
    }

    fmt.Println("\n--- Test 3: Age invalide ---")
    invalidAgeReq := UserRequest{
        "name":  "Pierre Martin",
        "email": "pierre.martin@example.com",
        "age":   "abc", // Âge non numérique
    }
    msg, err = validateAndProcessUserRequest(invalidAgeReq)
    if err != nil {
        fmt.Println("Erreur lors de la validation:", err) // Attendu: l'âge doit être un nombre valide
    } else {
        fmt.Println("Succès:", msg)
    }
}

Explication de l'exemple :

  1. UserRequest map[string]string: Bien que les structs soient plus courantes pour les données d'API, une map est utilisée ici pour démontrer l'itération et l'accès aux données.
  2. validateAndProcessUserRequest fonction:
    • Prend un UserRequest et retourne un string (message) et un error (gestion standard des erreurs en Go).
    • Validation des champs obligatoires (for-range et if): Itère sur une slice de noms de champs. Pour chaque champ, elle vérifie s'il existe dans la map et s'il n'est pas vide après avoir retiré les espaces.
    • Validation de l'e-mail (if): Vérifie la présence de @ et . (simplifié).
    • Validation de l'âge (strconv.Atoi, if err != nil): Utilise strconv.Atoi pour convertir la chaîne ageStr en int. C'est un exemple clé de conversion de type explicite et de la gestion des erreurs avec les retours multiples. Des conditions if supplémentaires vérifient la plage d'âge.
    • Message de bienvenue (createWelcomeMessage): Appel à une autre fonction qui utilise un switch sans condition pour personnaliser le message.
    • defer: Simule l'ouverture et la fermeture d'une connexion à une base de données, garantissant que la fermeture se produit toujours, même si une erreur survient plus tôt dans la fonction.
  3. createWelcomeMessage fonction: Démontre l'utilisation d'un switch sans expression pour des logiques conditionnelles basées sur des plages de valeurs.
  4. main fonction: Appelle validateAndProcessUserRequest avec différentes requêtes pour illustrer les chemins de succès et d'erreur. La vérification if err != nil est cruciale après chaque appel de fonction qui peut retourner une erreur.

Conclusion

Cette leçon vous a introduit aux fondations de la programmation en Go :

  • Les types de données comme les nombres, les booléens et les chaînes, ainsi que les méthodes de déclaration et de conversion de variables.
  • Les fonctions, qui sont le moyen principal d'organiser votre code, avec un accent particulier sur les retours multiples pour la gestion des erreurs et l'utilisation de defer pour le nettoyage des ressources.
  • Les instructions de contrôle de flux (if, switch, for) qui vous permettent de dicter la logique d'exécution de votre programme.

La maîtrise de ces concepts est la première étape indispensable pour construire des applications backend robustes et performantes avec Go. Dans les leçons suivantes, nous approfondirons des sujets tels que les structures (structs), les interfaces, les goroutines et les canaux, qui sont des aspects clés de Go pour le développement d'APIs performantes et scalables. Entraînez-vous avec les exemples fournis et n'hésitez pas à expérimenter pour solidifier votre compréhension !