GOG - Practice

TP Go : Structs, Interfaces, Architecture et Bonnes Pratiques

Objectif global du TP

Ce TP a pour but de comprendre et pratiquer les fondamentaux de Go :

  • Structs et méthodes
  • Récepteurs valeur vs pointeur
  • Interfaces et polymorphisme
  • Composition
  • Gestion des erreurs
  • Organisation en packages

Partie 1 : Structs et instances

Sujet

  1. Créer un programme Go minimal.
  2. Définir une struct User avec :
    • ID
    • Name
    • Email
  3. Créer une instance de User.
  4. Afficher son contenu dans main.

Solution

package main

import "fmt"

type User struct {
    ID    int
    Name  string
    Email string
}

func main() {
    user := User{
        ID:    1,
        Name:  "Alice",
        Email: "alice@example.com",
    }

    fmt.Println(user)
}

Partie 2 : Méthodes sur une struct

Sujet

  1. Ajouter une méthode Display() à User.
  2. Cette méthode affiche proprement les champs.
  3. Appeler cette méthode depuis main.

Solution

func (u User) Display() {
    fmt.Printf("ID: %d\nName: %s\nEmail: %s\n", u.ID, u.Name, u.Email)
}

func main() {
    user := User{1, "Alice", "alice@example.com"}
    user.Display()
}

Partie 3 : Récepteur valeur vs pointeur

Sujet

  1. Ajouter une méthode UpdateEmail.
  2. Tester avec un récepteur valeur.
  3. Tester avec un récepteur pointeur.
  4. Observer la différence.

Solution

func (u User) UpdateEmailValue(email string) {
    u.Email = email
}

func (u *User) UpdateEmailPointer(email string) {
    u.Email = email
}

func main() {
    user := User{1, "Alice", "alice@example.com"}

    user.UpdateEmailValue("value@test.com")
    fmt.Println(user.Email)

    user.UpdateEmailPointer("pointer@test.com")
    fmt.Println(user.Email)
}

Partie 4 : Interfaces et polymorphisme

Sujet

  1. Créer une interface Notifier.
  2. Elle expose Send(message string).
  3. Créer deux implémentations :
    • EmailNotifier
    • SMSNotifier
  4. Utiliser l’interface de manière polymorphe.

Solution

type Notifier interface {
    Send(message string)
}

type EmailNotifier struct{}

func (e EmailNotifier) Send(message string) {
    fmt.Println("Email:", message)
}

type SMSNotifier struct{}

func (s SMSNotifier) Send(message string) {
    fmt.Println("SMS:", message)
}

func NotifyAll(notifiers []Notifier, message string) {
    for _, n := range notifiers {
        n.Send(message)
    }
}

func main() {
    notifiers := []Notifier{
        EmailNotifier{},
        SMSNotifier{},
    }

    NotifyAll(notifiers, "Hello Go")
}

Partie 5 : Composition de structs

Sujet

  1. Créer une struct BaseEntity avec :
    • ID
    • CreatedAt
  2. Créer une struct Order qui compose BaseEntity.
  3. Accéder directement aux champs embarqués.

Solution

import "time"

type BaseEntity struct {
    ID        int
    CreatedAt time.Time
}

type Order struct {
    BaseEntity
    Total float64
}

func main() {
    order := Order{
        BaseEntity: BaseEntity{
            ID:        10,
            CreatedAt: time.Now(),
        },
        Total: 99.99,
    }

    fmt.Println(order.ID)
    fmt.Println(order.CreatedAt)
    fmt.Println(order.Total)
}

Partie 6 : Gestion des erreurs

Sujet

  1. Créer une fonction Divide(a, b int).
  2. Retourner une erreur si b == 0.
  3. Utiliser une erreur personnalisée.
  4. Ajouter du contexte avec wrapping.

Solution

import (
    "errors"
    "fmt"
)

var ErrDivisionByZero = errors.New("division by zero")

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide failed: %w", ErrDivisionByZero)
    }
    return a / b, nil
}

func main() {
    result, err := Divide(10, 0)
    if err != nil {
        if errors.Is(err, ErrDivisionByZero) {
            fmt.Println("Erreur: division par zéro")
        }
        return
    }
    fmt.Println(result)
}

Partie 7 : Organisation en packages

Sujet

  1. Créer les packages :
    • models
    • services
  2. Déplacer les structs et la logique métier.
  3. Respecter la visibilité Go (majuscule / minuscule).

Solution

models/user.go

package models

type User struct {
    ID    int
    Name  string
    Email string
}

services/user_service.go

package services

import "tp/models"

func CreateUser(id int, name, email string) models.User {
    return models.User{
        ID:    id,
        Name:  name,
        Email: email,
    }
}

main.go

package main

import (
    "fmt"
    "tp/services"
)

func main() {
    user := services.CreateUser(1, "Alice", "alice@example.com")
    fmt.Println(user)
}

Partie 8 : TP de synthèse

Sujet

Créer une application Go qui :

  • utilise des structs métiers
  • expose des méthodes
  • utilise des interfaces
  • gère les erreurs proprement
  • est découpée en packages

Solution (architecture cible)

project/
│
├── main.go
├── models/
│   └── user.go
├── services/
│   └── notifier.go
└── utils/
    └── errors.go

TP Go : Goroutines, Channels et WaitGroup

Objectif global du TP

Ce TP permet de comprendre et pratiquer la concurrence en Go :

  • goroutines
  • channels
  • synchronisation avec sync.WaitGroup
  • patterns classiques (fan-out / fan-in, worker pool)
  • erreurs courantes (deadlock, goroutines orphelines)

Partie 1 : Découverte des goroutines

Sujet

  1. Créer un programme Go simple.
  2. Lancer une fonction dans une goroutine.
  3. Observer le comportement du programme.
  4. Empêcher le programme de se terminer trop tôt.

Solution

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine")
}

func main() {
    go sayHello()

    time.Sleep(1 * time.Second)
    fmt.Println("Main finished")
}

Partie 2 : Lancement concurrent de plusieurs goroutines

Sujet

  1. Lancer plusieurs goroutines dans une boucle.
  2. Chaque goroutine affiche un numéro.
  3. Observer l’ordre d’exécution.
  4. Mettre les variables dans l'odre (optionnel).

Solution

package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    fmt.Println("Worker", id)
}

func main() {
    for i := 0; i < 5; i++ {
        go worker(i)
    }

    time.Sleep(1 * time.Second)
}

Partie 3 : Introduction aux channels

Sujet

  1. Créer un channel de type string.
  2. Envoyer un message depuis une goroutine.
  3. Recevoir le message dans main.

Solution

package main

import "fmt"

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Hello via channel"
    }()

    msg := <-ch
    fmt.Println(msg)
}

Partie 4 : Channels bloquants

Sujet

  1. Observer le comportement bloquant d’un channel.
  2. Comprendre pourquoi l’envoi ou la réception bloque.
  3. Modifier le code pour éviter le blocage.

Solution

package main

import "fmt"

func main() {
    ch := make(chan int)

    go func() {
        ch <- 42
    }()

    value := <-ch
    fmt.Println(value)
}

Partie 5 : Channels bufferisés

Sujet

  1. Créer un channel avec un buffer.
  2. Envoyer plusieurs valeurs sans bloquer.
  3. Lire les valeurs ensuite.

Solution

package main

import "fmt"

func main() {
    ch := make(chan int, 3)

    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Partie 6 : Synchronisation avec WaitGroup

Sujet

  1. Créer un sync.WaitGroup.
  2. Lancer plusieurs goroutines.
  3. Attendre leur fin proprement.

Solution

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Worker", id, "done")
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers finished")
}

Partie 7 : Combiner goroutines, channels et WaitGroup

Sujet

  1. Lancer plusieurs goroutines productrices.
  2. Envoyer des données dans un channel.
  3. Lire toutes les données.
  4. Fermer correctement le channel.

Solution

package main

import (
    "fmt"
    "sync"
)

func producer(id int, ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    ch <- id * 10
}

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go producer(i, ch, &wg)
    }

    go func() {
        wg.Wait()
        close(ch)
    }()

    for value := range ch {
        fmt.Println("Received:", value)
    }
}

Partie 8 : Pattern Worker Pool

Sujet

  1. Créer un pool de workers.
  2. Distribuer des tâches via un channel.
  3. Collecter les résultats.
  4. Synchroniser la fin du traitement.

Solution

package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        results <- job * 2
    }
}

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

    var wg sync.WaitGroup

    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

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

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Result:", r)
    }
}

Partie 9 : Erreurs courantes et bonnes pratiques

Sujet

  1. Identifier les causes possibles de deadlock.
  2. Expliquer pourquoi oublier close() est dangereux.
  3. Expliquer le risque de goroutines orphelines.

Solution (théorie)

  • Deadlock : channel jamais lu ou jamais écrit
  • close() :
    • permet au consommateur de sortir de range
  • Goroutine orpheline :
    • goroutine bloquée sans possibilité de sortie
  • Toujours :
    • qui ferme le channel = le producteur
    • préférer WaitGroup à time.Sleep

TP Go : Reflection, Manipulation de fichiers et Réseau


TP 1 : Reflection en Go

Objectif

Apprendre à inspecter et manipuler les types en Go avec le package reflect :

  • inspecter les structs et leurs champs
  • lire et modifier des valeurs dynamiquement
  • utiliser Kind, Type, Value

Partie 1 : Découverte de reflect

Sujet

  1. Créer une struct Person avec Name string et Age int.
  2. Utiliser reflect.TypeOf et reflect.ValueOf pour afficher le type et la valeur de la struct.

Solution

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{"Alice", 30}
    t := reflect.TypeOf(p)
    v := reflect.ValueOf(p)

    fmt.Println("Type:", t)
    fmt.Println("Value:", v)
}

Partie 2 : Inspection des champs d’une struct

Sujet

  1. Parcourir les champs de la struct avec NumField et Field(i).
  2. Afficher le nom et la valeur de chaque champ.

Solution

t := reflect.TypeOf(p)
v := reflect.ValueOf(p)

for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    value := v.Field(i)
    fmt.Printf("%s = %v\n", field.Name, value)
}

Partie 3 : Modification de valeur avec reflect

Sujet

  1. Modifier dynamiquement le champ Age de Person.
  2. Rappel : il faut un Value adressable (reflect.ValueOf(&p).Elem()).

Solution

vp := reflect.ValueOf(&p).Elem()
ageField := vp.FieldByName("Age")
if ageField.CanSet() {
    ageField.SetInt(35)
}
fmt.Println(p)

TP 2 : Manipulation de fichiers

Objectif

Apprendre à :

  • lire et écrire des fichiers texte
  • lire/écrire CSV et JSON
  • gérer les erreurs

Partie 1 : Fichier texte simple

Sujet

  1. Créer un fichier test.txt.
  2. Écrire quelques lignes.
  3. Lire le fichier et afficher chaque ligne.

Solution

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

func main() {
    f, _ := os.Create("test.txt")
    defer f.Close()

    f.WriteString("Ligne 1\nLigne 2\nLigne 3\n")

    fRead, _ := os.Open("test.txt")
    defer fRead.Close()

    scanner := bufio.NewScanner(fRead)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

Partie 2 : CSV

Sujet

  1. Créer un fichier CSV data.csv avec Name,Age.
  2. Lire et afficher chaque ligne.
  3. Ajouter une nouvelle ligne.

Solution

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

func main() {
    f, _ := os.Create("data.csv")
    defer f.Close()

    writer := csv.NewWriter(f)
    writer.Write([]string{"Name", "Age"})
    writer.Write([]string{"Alice", "30"})
    writer.Flush()

    fRead, _ := os.Open("data.csv")
    defer fRead.Close()

    reader := csv.NewReader(fRead)
    records, _ := reader.ReadAll()
    for _, record := range records {
        fmt.Println(record)
    }
}

Partie 3 : JSON

Sujet

  1. Créer une struct Person.
  2. Écrire un slice de Person dans un fichier JSON.
  3. Lire le JSON et l’afficher.

Solution

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

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
    }

    f, _ := os.Create("people.json")
    defer f.Close()
    json.NewEncoder(f).Encode(people)

    fRead, _ := os.Open("people.json")
    defer fRead.Close()

    var loaded []Person
    json.NewDecoder(fRead).Decode(&loaded)
    fmt.Println(loaded)
}

TP 3 : Réseau (TCP/UDP)

Objectif

Apprendre à :

  • créer un serveur TCP
  • créer un client TCP
  • envoyer et recevoir des données

Partie 1 : Serveur TCP

Sujet

  1. Créer un serveur TCP sur le port 9000.
  2. Recevoir des messages et les afficher.

Solution

import (
    "fmt"
    "net"
)

func main() {
    ln, _ := net.Listen("tcp", ":9000")
    defer ln.Close()
    fmt.Println("Serveur TCP en écoute sur :9000")

    for {
        conn, _ := ln.Accept()
        go func(c net.Conn) {
            buf := make([]byte, 1024)
            n, _ := c.Read(buf)
            fmt.Println("Reçu :", string(buf[:n]))
            c.Close()
        }(conn)
    }
}

Partie 2 : Client TCP

Sujet

  1. Créer un client TCP.
  2. Envoyer un message au serveur.

Solution

import (
    "fmt"
    "net"
)

func main() {
    conn, _ := net.Dial("tcp", "127.0.0.1:9000")
    defer conn.Close()
    message := "Hello server"
    fmt.Fprintln(conn, message)
}

Partie 3 : UDP simple

Sujet

  1. Créer un serveur UDP.
  2. Créer un client UDP.
  3. Envoyer et recevoir un message.

Solution

Serveur UDP :

addr, _ := net.ResolveUDPAddr("udp", ":9001")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()

buf := make([]byte, 1024)
n, clientAddr, _ := conn.ReadFromUDP(buf)
fmt.Println("Reçu:", string(buf[:n]), "de", clientAddr)

Client UDP :

serverAddr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:9001")
conn, _ := net.DialUDP("udp", nil, serverAddr)
defer conn.Close()

conn.Write([]byte("Hello UDP"))

TP Go :API Web avec Gin, GORM et YAML

Objectif global du TP

Ce TP a pour objectif de créer une mini-API REST Go avec :

  • Gin-Gonic pour le serveur web
  • GORM pour la base de données (SQLite pour simplifier)
  • Structs et méthodes Go
  • Lecture de configuration depuis un fichier YAML

Partie 1 :Préparation du projet

Sujet

  1. Créer un dossier tp_gin_gorm.
  2. Créer un fichier main.go.
  3. Installer les dépendances :
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
go get -u gopkg.in/yaml.v3
  1. Créer un fichier config.yaml avec le contenu suivant :
server:
  port: 8080

database:
  path: "test.db"

Solution

structure du projet :

tp_gin_gorm/
│
├── main.go
├── config.yaml
├── models/
│   └── user.go
└── go.mod

Partie 2 :Lecture de fichier YAML

Sujet

  1. Lire le fichier config.yaml.
  2. Mapper le contenu dans une struct Config.
  3. Afficher la configuration lue dans main().

Solution

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "io/ioutil"
)

type Config struct {
    Server struct {
        Port int `yaml:"port"`
    } `yaml:"server"`
    Database struct {
        Path string `yaml:"path"`
    } `yaml:"database"`
}

func LoadConfig(path string) (Config, error) {
    var cfg Config
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return cfg, err
    }
    err = yaml.Unmarshal(data, &cfg)
    return cfg, err
}

func main() {
    cfg, err := LoadConfig("config.yaml")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Server port: %d\nDatabase path: %s\n", cfg.Server.Port, cfg.Database.Path)
}

Partie 3 :Structs et modèles GORM

Sujet

  1. Créer un modèle User avec les champs :
    • ID (auto-increment)
    • Name (string)
    • Email (string, unique)
  2. Initialiser la base SQLite avec GORM et migrer le modèle.

Solution

models/user.go

package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Name  string `gorm:"size:100"`
    Email string `gorm:"unique"`
}

main.go (ajout GORM)

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "tp_gin_gorm/models"
)

func InitDB(path string) *gorm.DB {
    db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    db.AutoMigrate(&models.User{})
    return db
}

func main() {
    cfg, _ := LoadConfig("config.yaml")
    db := InitDB(cfg.Database.Path)
    fmt.Println("Database initialized:", db)
}

Partie 4 :Création de l’API Gin

Sujet

  1. Créer un serveur Gin sur le port du fichier YAML.
  2. Ajouter les endpoints CRUD pour User :
    • GET /users → liste tous les utilisateurs
    • POST /users → créer un utilisateur
    • GET /users/:id → récupérer un utilisateur
    • PUT /users/:id → mettre à jour un utilisateur
    • DELETE /users/:id → supprimer un utilisateur

Solution

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "tp_gin_gorm/models"
)

func main() {
    cfg, _ := LoadConfig("config.yaml")
    db := InitDB(cfg.Database.Path)
    r := gin.Default()

    r.GET("/users", func(c *gin.Context) {
        var users []models.User
        db.Find(&users)
        c.JSON(http.StatusOK, users)
    })

    r.POST("/users", func(c *gin.Context) {
        var user models.User
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        db.Create(&user)
        c.JSON(http.StatusOK, user)
    })

    r.GET("/users/:id", func(c *gin.Context) {
        var user models.User
        if err := db.First(&user, c.Param("id")).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }
        c.JSON(http.StatusOK, user)
    })

    r.PUT("/users/:id", func(c *gin.Context) {
        var user models.User
        if err := db.First(&user, c.Param("id")).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }
        var input models.User
        if err := c.ShouldBindJSON(&input); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        db.Model(&user).Updates(input)
        c.JSON(http.StatusOK, user)
    })

    r.DELETE("/users/:id", func(c *gin.Context) {
        var user models.User
        if err := db.First(&user, c.Param("id")).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
            return
        }
        db.Delete(&user)
        c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
    })

    r.Run(fmt.Sprintf(":%d", cfg.Server.Port))
}

Partie 5 :Test de l’API

Sujet

  1. Lancer le serveur.
  2. Tester avec curl ou Postman :
  • Créer un utilisateur :
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"Name":"Alice","Email":"alice@example.com"}'
  • Lister les utilisateurs :
curl http://localhost:8080/users
  • Modifier, récupérer, supprimer : idem.

Mise en place GORM

docker run -p 5432:5432 --name some-postgres -e POSTGRES_DB=gormdb -e POSTGRES_USER=gormuser -e POSTGRES_PASSWORD=gormpassword -d postgres
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
package main

import (
	"fmt"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"log"
)

var DB *gorm.DB

func initDatabase() {
	dsn := "host=localhost user=gormuser password=gormpassword dbname=gormdb port=5432 sslmode=disable TimeZone=Europe/Paris"
	var err error
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database: ", err)
	}
	fmt.Println("Database connected successfully!")
}

func main() {
	initDatabase()
}

type User struct {
	ID       uint   `gorm:"primaryKey"`
	Name     string
	Email    string  `gorm:"unique"`
	Password string
}

func migrate() {
	DB.AutoMigrate(&User{})
	fmt.Println("Database migrated!")
}


func main() {
	initDatabase()
	migrate()
}

func createUser(name string, email string, password string) {
	user := User{Name: name, Email: email, Password: password}
	DB.Create(&user)
	fmt.Printf("User %s created successfully!\n", user.Name)
}

// Dans main

createUser("John Doe", "john@example.com", "securepassword")
func getUsers() {
	var users []User
	DB.Find(&users)
	for _, user := range users {
		fmt.Printf("User: %s, Email: %s\n", user.Name, user.Email)
	}
}

func updateUserEmail(id uint, newEmail string) {
	var user User
	DB.First(&user, id)
	user.Email = newEmail
	DB.Save(&user)
	fmt.Printf("User %s updated with new email: %s\n", user.Name, user.Email)
}

// Dans main

updateUserEmail(1, "john.doe@newemail.com")

func deleteUser(id uint) {
	var user User
	DB.Delete(&user, id)
	fmt.Printf("User with ID %d deleted successfully!\n", id)
}

// Dans main
deleteUser(1)



Mise en place Golang avec Redis

# Executer redis 
docker run --name redis-cache -d -p 6379:6379 redis

# Executer redis-cli
docker exec -it redis-cache redis-cli ping

# Créer le repo
mkdir go-redis-cache
cd go-redis-cache
go mod init go-redis-cache

# Gin-Gonic
go get -u github.com/gin-gonic/gin
go get -u github.com/go-redis/redis/v8

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"context"
	"github.com/go-redis/redis/v8"
	"time"
)

var ctx = context.Background()

func main() {
	r := gin.Default()

	// Connecter Redis
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // Pas de mot de passe par défaut
		DB:       0,  // Utiliser la base de données par défaut
	})

	// Route pour obtenir une donnée
	r.GET("/data/:key", func(c *gin.Context) {
		key := c.Param("key")

		// Vérifier si la donnée est en cache
		val, err := rdb.Get(ctx, key).Result()
		if err == redis.Nil {
			// Simuler une récupération lente de la donnée depuis la base de données
			time.Sleep(2 * time.Second)
			val = fmt.Sprintf("Data for key %s", key)

			// Stocker la donnée dans Redis avec une durée d'expiration
			err := rdb.Set(ctx, key, val, 10*time.Second).Err()
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cache data"})
				return
			}
		} else if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve data"})
			return
		}

		// Retourner la donnée (du cache ou de la "base de données")
		c.JSON(http.StatusOK, gin.H{"data": val})
	})

	// Démarrer le serveur HTTP
	r.Run(":8080")
}

Mise en place Gin-Gonic

go get -u github.com/gin-gonic/gin
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// Définir une route GET
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	// Démarrer le serveur sur le port 8080
	r.Run(":8080")
}
go run main.go
// Création de routes et gestion des requêtes
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

var users = []string{"Alice", "Bob", "Charlie"}

func main() {
	r := gin.Default()

	// Route pour obtenir tous les utilisateurs
	r.GET("/users", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"users": users,
		})
	})

	// Route pour ajouter un utilisateur
	r.POST("/users", func(c *gin.Context) {
		var newUser string
		if err := c.BindJSON(&newUser); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid data"})
			return
		}
		users = append(users, newUser)
		c.JSON(http.StatusCreated, gin.H{"user": newUser})
	})

	// Démarrer le serveur
	r.Run(":8080")
}

func LoggerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("Request received: ", c.Request.Method, c.Request.URL)
		c.Next()
	}
}

func main() {
	r := gin.Default()

	// Utiliser le middleware de journalisation
	r.Use(LoggerMiddleware())

	// Définir des routes...
	r.Run(":8080")
}
r.POST("/users", func(c *gin.Context) {
	var newUser string
	if err := c.BindJSON(&newUser); err != nil || newUser == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"})
		return
	}
	users = append(users, newUser)
	c.JSON(http.StatusCreated, gin.H{"user": newUser})
})

r.GET("/error", func(c *gin.Context) {
	c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
})
package main

import (
	"bytes"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestGetUsers(t *testing.T) {
	// Configurer Gin en mode test
	r := gin.Default()

	// Route de test
	r.GET("/users", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"users": users,
		})
	})

	// Créer une requête HTTP
	req, _ := http.NewRequest("GET", "/users", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	// Assertions
	assert.Equal(t, http.StatusOK, w.Code)
	assert.Contains(t, w.Body.String(), "Alice")
}

func TestPostUser(t *testing.T) {
	// Configurer Gin en mode test
	r := gin.Default()

	// Route de test
	r.POST("/users", func(c *gin.Context) {
		var newUser string
		if err := c.BindJSON(&newUser); err != nil || newUser == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"})
			return
		}
		users = append(users, newUser)
		c.JSON(http.StatusCreated, gin.H{"user": newUser})
	})

	// Créer une requête HTTP
	user := `"David"`
	req, _ := http.NewRequest("POST", "/users", bytes.NewBufferString(user))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	// Assertions
	assert.Equal(t, http.StatusCreated, w.Code)
	assert.Contains(t, w.Body.String(), "David")
}
go test

TP Go : Backend complet REST + gRPC avec Auth JWT

Objectif global

Créer un backend professionnel en Go combinant :

  • API REST publique avec Gin-Gonic
  • API gRPC interne
  • Base de données avec GORM
  • Configuration via fichier YAML
  • Authentification JWT
  • Architecture modulaire et maintenable

Ce TP simule un vrai backend d’entreprise.


Architecture cible

backend/
│
├── cmd/
│   └── api/
│       └── main.go
│
├── config/
│   └── config.yaml
│
├── internal/
│   ├── config/
│   │   └── loader.go
│   ├── database/
│   │   └── db.go
│   ├── models/
│   │   ├── user.go
│   │   └── order.go
│   ├── repositories/
│   │   └── user_repo.go
│   ├── services/
│   │   ├── auth.go
│   │   └── notifier.go
│   ├── http/
│   │   ├── handlers/
│   │   │   └── user_handler.go
│   │   └── middleware/
│   │       └── jwt.go
│   └── grpc/
│       ├── server.go
│       └── proto/
│           └── user.proto
│
├── go.mod
└── go.sum

Partie 1 : Configuration YAML

Sujet

  1. Créer un fichier config.yaml.
  2. Configurer serveur HTTP, gRPC, base de données et JWT.
  3. Charger la configuration au démarrage.

Solution

server:
  http_port: 8080
  grpc_port: 50051

database:
  driver: sqlite
  path: app.db

jwt:
  secret: supersecretkey
  ttl: 3600
type Config struct {
    Server struct {
        HTTPPort int `yaml:"http_port"`
        GRPCPort int `yaml:"grpc_port"`
    } `yaml:"server"`
    Database struct {
        Path string `yaml:"path"`
    } `yaml:"database"`
    JWT struct {
        Secret string `yaml:"secret"`
        TTL    int    `yaml:"ttl"`
    } `yaml:"jwt"`
}

Partie 2 : Base de données avec GORM

Sujet

  1. Initialiser la DB avec GORM.
  2. Créer les modèles User et Order.
  3. Migrer automatiquement les tables.

Solution

type User struct {
    gorm.Model
    Email    string `gorm:"unique"`
    Password string
}

type Order struct {
    gorm.Model
    UserID uint
    Amount float64
}
db, _ := gorm.Open(sqlite.Open(cfg.Database.Path))
db.AutoMigrate(&User{}, &Order{})

Partie 3 : Authentification JWT

Sujet

  1. Créer une fonction de génération de JWT.
  2. Ajouter un middleware Gin pour valider le token.
  3. Protéger certaines routes.

Solution

func GenerateJWT(userID uint, secret string) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id": userID,
        "exp":     time.Now().Add(time.Hour).Unix(),
    })
    return token.SignedString([]byte(secret))
}
func JWTMiddleware(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenStr := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            return []byte(secret), nil
        })
        if err != nil || !token.Valid {
            c.AbortWithStatus(401)
            return
        }
        c.Next()
    }
}

Partie 4 : API REST avec Gin-Gonic

Sujet

  1. Créer endpoints REST :
    • POST /login
    • GET /users (protégé)
  2. Utiliser JWT pour sécuriser l’accès.

Solution

r := gin.Default()

r.POST("/login", func(c *gin.Context) {
    var u User
    c.BindJSON(&u)
    token, _ := GenerateJWT(1, cfg.JWT.Secret)
    c.JSON(200, gin.H{"token": token})
})

auth := r.Group("/api", JWTMiddleware(cfg.JWT.Secret))
auth.GET("/users", func(c *gin.Context) {
    var users []User
    db.Find(&users)
    c.JSON(200, users)
})

Partie 5 : Service gRPC interne

Sujet

  1. Créer un service gRPC UserService.
  2. Exposer une méthode GetUser.
  3. Utiliser la même DB que l’API REST.

Solution

syntax = "proto3";

package user;

option go_package = "backend/internal/grpc/proto";

message UserRequest {
    uint32 id = 1;
}

message UserResponse {
    uint32 id = 1;
    string email = 2;
}

service UserService {
    rpc GetUser(UserRequest) returns (UserResponse);
}
func (s *GRPCServer) GetUser(ctx context.Context, req *proto.UserRequest) (*proto.UserResponse, error) {
    var user User
    s.db.First(&user, req.Id)
    return &proto.UserResponse{
        Id:    uint32(user.ID),
        Email: user.Email,
    }, nil
}

Partie 6 : Lancement REST + gRPC simultané

Sujet

  1. Lancer Gin et gRPC en parallèle.
  2. Utiliser WaitGroup pour la synchronisation.

Solution

var wg sync.WaitGroup
wg.Add(2)

go func() {
    defer wg.Done()
    r.Run(":8080")
}()

go func() {
    defer wg.Done()
    grpcServer.Serve(listener)
}()

wg.Wait()

Partie 7 : Notifications concurrentes

Sujet

  1. Créer une interface Notifier.
  2. Envoyer des notifications asynchrones après certaines actions.

Solution

type Notifier interface {
    Send(msg string)
}

type LogNotifier struct{}

func (l LogNotifier) Send(msg string) {
    fmt.Println("NOTIFY:", msg)
}
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        LogNotifier{}.Send(fmt.Sprintf("event %d", i))
    }(i)
}
wg.Wait()