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
- Créer un programme Go minimal.
- Définir une struct
Useravec :- ID
- Name
- Créer une instance de
User. - 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
- Ajouter une méthode
Display()àUser. - Cette méthode affiche proprement les champs.
- 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
- Ajouter une méthode
UpdateEmail. - Tester avec un récepteur valeur.
- Tester avec un récepteur pointeur.
- 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
- Créer une interface
Notifier. - Elle expose
Send(message string). - Créer deux implémentations :
- EmailNotifier
- SMSNotifier
- 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
- Créer une struct
BaseEntityavec :- ID
- CreatedAt
- Créer une struct
Orderqui composeBaseEntity. - 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
- Créer une fonction
Divide(a, b int). - Retourner une erreur si
b == 0. - Utiliser une erreur personnalisée.
- 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
- Créer les packages :
- models
- services
- Déplacer les structs et la logique métier.
- 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
- Créer un programme Go simple.
- Lancer une fonction dans une goroutine.
- Observer le comportement du programme.
- 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
- Lancer plusieurs goroutines dans une boucle.
- Chaque goroutine affiche un numéro.
- Observer l’ordre d’exécution.
- 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
- Créer un channel de type
string. - Envoyer un message depuis une goroutine.
- 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
- Observer le comportement bloquant d’un channel.
- Comprendre pourquoi l’envoi ou la réception bloque.
- 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
- Créer un channel avec un buffer.
- Envoyer plusieurs valeurs sans bloquer.
- 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
- Créer un
sync.WaitGroup. - Lancer plusieurs goroutines.
- 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
- Lancer plusieurs goroutines productrices.
- Envoyer des données dans un channel.
- Lire toutes les données.
- 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
- Créer un pool de workers.
- Distribuer des tâches via un channel.
- Collecter les résultats.
- 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
- Identifier les causes possibles de deadlock.
- Expliquer pourquoi oublier
close()est dangereux. - Expliquer le risque de goroutines orphelines.
Solution (théorie)
- Deadlock : channel jamais lu ou jamais écrit
close():- permet au consommateur de sortir de
range
- permet au consommateur de sortir de
- 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
- Créer une struct
PersonavecName stringetAge int. - Utiliser
reflect.TypeOfetreflect.ValueOfpour 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
- Parcourir les champs de la struct avec
NumFieldetField(i). - 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
- Modifier dynamiquement le champ
AgedePerson. - Rappel : il faut un
Valueadressable (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
- Créer un fichier
test.txt. - Écrire quelques lignes.
- 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
- Créer un fichier CSV
data.csvavecName,Age. - Lire et afficher chaque ligne.
- 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
- Créer une struct
Person. - Écrire un slice de
Persondans un fichier JSON. - 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
- Créer un serveur TCP sur le port 9000.
- 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
- Créer un client TCP.
- 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
- Créer un serveur UDP.
- Créer un client UDP.
- 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
- Créer un dossier
tp_gin_gorm. - Créer un fichier
main.go. - 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
- Créer un fichier
config.yamlavec 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
- Lire le fichier
config.yaml. - Mapper le contenu dans une struct
Config. - 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
- Créer un modèle
Useravec les champs :- ID (auto-increment)
- Name (string)
- Email (string, unique)
- 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
- Créer un serveur Gin sur le port du fichier YAML.
- 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
- Lancer le serveur.
- Tester avec
curlou 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 postgresgo 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
- Créer un fichier
config.yaml. - Configurer serveur HTTP, gRPC, base de données et JWT.
- 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
- Initialiser la DB avec GORM.
- Créer les modèles
UseretOrder. - 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
- Créer une fonction de génération de JWT.
- Ajouter un middleware Gin pour valider le token.
- 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
- Créer endpoints REST :
- POST /login
- GET /users (protégé)
- 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
- Créer un service gRPC
UserService. - Exposer une méthode
GetUser. - 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
- Lancer Gin et gRPC en parallèle.
- Utiliser
WaitGrouppour 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
- Créer une interface
Notifier. - 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()