MFormations
Modern Go Engineering

Chapitre 17

17 - Exercices

> Mettez en pratique vos compétences Go avec 40 exercices progressifs couvrant l'ensemble du spectre du développement Go moderne.

Cours 17 : Exercices pratiques Go

300+ lignes de contenu pédagogique avec 40 exercices progressifs


Introduction

Ce chapitre propose 40 exercices pratiques couvrant l'ensemble des concepts du langage Go, de la syntaxe de base jusqu'aux patterns avancés de concurrence, en passant par le développement d'APIs, l'accès aux bases de données, le testing, et le déploiement. Chaque exercice est conçu pour renforcer une compétence spécifique et comprend des objectifs d'apprentissage clairs.


Module 1 : Go Fundamentals (Exercices 01-05)

Exercice 01 : FizzBuzz avec gestion d'erreur

Objectif : Maîtriser les bases du contrôle de flux en Go.

Énoncé : Écrivez une fonction FizzBuzz(n int) (string, error) qui retourne :

  • "Fizz" si n est divisible par 3
  • "Buzz" si n est divisible par 5
  • "FizzBuzz" si n est divisible par 3 et 5
  • Le nombre en chaîne sinon

Gérez les cas d'erreur : n négatif, n nul, overflow.

Concepts clés :

  • switch / case
  • error interface
  • fmt.Sprintf
  • math.MaxInt / math.MinInt

Contraintes :

  • Utilisez switch sans expression (switch true)
  • Retournez une error descriptive avec fmt.Errorf
  • Pas de dépendances externes

Exercice 02 : Palindrome Unicode

Objectif : Manipuler les chaînes Unicode en Go.

Énoncé : Implémentez IsPalindrome(s string) bool qui vérifie si une chaîne est un palindrome en ignorant la casse et les caractères non-lettres. Gérez correctement l'Unicode.

Concepts clés :

  • unicode package
  • strings.Map / unicode.ToLower
  • Runes vs bytes
  • Unicode normalization (optionnel)

Indices :

func IsPalindrome(s string) bool {
    f := func(r rune) rune {
        if !unicode.IsLetter(r) {
            return -1
        }
        return unicode.ToLower(r)
    }
    cleaned := strings.Map(f, s)
    // Comparer cleaned avec son inverse...
}

Exercice 03 : Shape Interface

Objectif : Comprendre le système d'interfaces Go.

Énoncé : Définissez une interface Shape avec les méthodes Area() float64 et Perimeter() float64. Implémentez les types Circle, Rectangle, Triangle. Ajoutez une fonction TotalArea(shapes ...Shape) float64.

Concepts clés :

  • Interface definition
  • Method sets
  • Type assertion
  • Variadic functions

Extensions :

  • Ajoutez String() string pour chaque forme
  • Implémentez sort.Interface pour trier par aire

Exercice 04 : JSON Processing

Objectif : Maîtriser l'encodage/décodage JSON.

Énoncé : À partir de la structure :

type Person struct {
    Name    string   `json:"name"`
    Age     int      `json:"age"`
    Emails  []string `json:"emails,omitempty"`
    Address *Address `json:"address,omitempty"`
}

Implémentez :

  1. ParsePerson(data []byte) (Person, error) avec json.Unmarshal
  2. SerializePerson(p Person) ([]byte, error) avec json.MarshalIndent
  3. Un json.Marshaler personnalisé qui chiffre l'âge

Concepts clés :

  • Struct tags
  • json.Marshal / json.Unmarshal
  • Custom marshal/unmarshal
  • io.Reader / io.Writer

Exercice 05 : Custom Sorting

Objectif : Utiliser sort et les fermetures (closures).

Énoncé : Soit la structure Product{Name string, Price float64, Rating float64}. Implémentez le tri par :

  • Nom (ascendant)
  • Prix (descendant)
  • Rating (ascendant)
  • Note composée (weighted score)

Utilisez sort.Slice avec des closures.

Concepts clés :

  • sort.Slice
  • Closures capturant des variables
  • sort.Sort avec interface personnalisée

Module 2 : Goroutines & Channels (Exercices 06-11)

Exercice 06 : Worker Pool

Objectif : Implémenter le pattern worker pool.

Énoncé : Créez un WorkerPool qui :

  • Accepte N workers via un channel de jobs
  • Chaque worker traite un job et envoie le résultat
  • Supporte l'arrêt via context.Context
  • Gère le graceful shutdown

Signature :

type Job func(ctx context.Context) (Result, error)
type Result struct {
    Value interface{}
    Err   error
}

func NewPool(ctx context.Context, workers int) *Pool
func (p *Pool) Submit(job Job)
func (p *Pool) Results() <-chan Result
func (p *Pool) Wait()

Concepts clés :

  • Goroutines et channels
  • sync.WaitGroup
  • context.Context pour annulation
  • Buffered vs unbuffered channels

Exercice 07 : Pipeline

Objectif : Chaîner des étapes de traitement avec des channels.

Énoncé : Implémentez un pipeline à 3 étapes :

  1. Generate(numbers ...int) <-chan int : émet des nombres
  2. MultiplyBy2(in <-chan int) <-chan int : multiplie par 2
  3. FilterEven(in <-chan int) <-chan int : filtre les pairs

Connectez les étapes et affichez le résultat.

Concepts clés :

  • Channel direction (<-chan, chan<-)
  • Range over channels
  • Goroutine lifecycle
  • Pipeline pattern

Exercice 08 : Rate Limiter

Objectif : Implémenter un token bucket rate limiter.

Énoncé : Créez un RateLimiter qui limite à N opérations par seconde avec burst autorisé. Utilisez un time.Ticker pour remplir le bucket.

Signature :

func NewRateLimiter(rate int, burst int) *RateLimiter
func (rl *RateLimiter) Allow() bool
func (rl *RateLimiter) Wait(ctx context.Context) error

Concepts clés :

  • time.Ticker
  • time.NewTicker
  • Token bucket algorithm
  • Select with default

Exercice 09 : Timeout avec Context

Objectif : Maîtriser context.Context pour les timeouts et l'annulation.

Énoncé : Écrivez une fonction FetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (string, error) qui simule un appel HTTP long. Si le timeout expire, annulez et retournez une erreur.

Concepts clés :

  • context.WithTimeout
  • context.WithCancel
  • select avec <-ctx.Done()
  • Propagation de l'annulation

Exercice 10 : Merge Channels

Objectif : Fusionner dynamiquement N channels.

Énoncé : Implémentez Merge(channels ...<-chan int) <-chan int qui fusionne N channels en un seul. Tous les messages de tous les channels d'entrée doivent être forwarded vers le channel de sortie.

Deux approches sont attendues :

  1. Avec sync.WaitGroup et goroutines séparées
  2. Avec un pattern de select et reflect.Select

Concepts clés :

  • Fan-in pattern
  • sync.WaitGroup
  • reflect.Select pour le cas dynamique
  • Gestion de fermeture des channels

Exercice 11 : Prime Sieve (Crible d'Ératosthène)

Objectif : Implémenter un algorithme classique en concurrent.

Énoncé : Implémentez le crible d'Ératosthène concurrent de Go :

func Generate(ch chan<- int)
func Filter(in <-chan int, out chan<- int, prime int)
func Sieve(limit int) []int

Chaque nombre premier lance un nouveau filtre (goroutine).

Concepts clés :

  • Infinite channel communication
  • Dynamic goroutine creation
  • Prime sieve pattern (Rob Pike)

Module 3 : Concurrency Patterns (Exercices 12-15)

Exercice 12 : Pub/Sub

Objectif : Implémenter un bus publish-subscribe thread-safe.

Énoncé : Créez un PubSub générique qui supporte :

  • Subscribe(topic string) <-chan interface{}
  • Publish(topic string, msg interface{})
  • Unsubscribe(topic string, ch <-chan interface{})
  • Close() (ferme tous les channels proprement)

Concepts clés :

  • sync.RWMutex pour l'accès concurrent
  • Map de topic -> []chan
  • Éviter les fuites de goroutines
  • Broadcasting via range sur les subscribers

Exercice 13 : Circuit Breaker

Objectif : Pattern de résilience avec circuit breaker.

Énoncé : Implémentez un circuit breaker avec 3 états : Closed, Open, HalfOpen.

  • Closed : les appels passent, après N échecs -> Open
  • Open : les appels échouent immédiatement, après timeout -> HalfOpen
  • HalfOpen : un appel test, si succès -> Closed, si échec -> Open

Signature :

type CircuitBreaker struct {
    // ...
}
func NewCircuitBreaker(maxFailures int, resetTimeout time.Duration) *CircuitBreaker
func (cb *CircuitBreaker) Execute(fn func() error) error

Concepts clés :

  • State machine
  • sync.Mutex pour la protection
  • Atomic operations
  • Pattern de résilience

Exercice 14 : Fan-Out/Fan-In

Objectif : Paralléliser le traitement avec agrégation.

Énoncé : Implémentez un système de traitement de mots :

  1. FanOut(in <-chan string, workers int) []<-chan string : distribue le travail
  2. FanIn(channels ...<-chan string) <-chan string : agrège les résultats
  3. Les workers comptent les lettres de chaque mot

Concepts clés :

  • Parallélisation déterministe
  • Distribution équitable (round-robin)
  • Agrégation correcte des résultats
  • Gestion des goroutines zombies

Exercice 15 : Pipeline with Error Handling

Objectif : Pipeline avec gestion d'erreurs.

Énoncé : Étendez le pipeline de l'exercice 07 avec :

  • Un channel d'erreurs séparé
  • Un error group (golang.org/x/sync/errgroup)
  • Un mécanisme de retry (3 tentatives max)
  • Un reporting des erreurs à la fin

Concepts clés :

  • errgroup.Group
  • Error channel pattern
  • Retry with backoff
  • Graceful degradation

Module 4 : HTTP API (Exercices 16-19)

Exercice 16 : REST API CRUD

Objectif : Construire une API REST complète.

Énoncé : Créez une API REST pour la ressource Book :

  • GET /api/books : liste paginée
  • GET /api/books/:id : détail
  • POST /api/books : création
  • PUT /api/books/:id : mise à jour
  • DELETE /api/books/:id : suppression

Stockage en mémoire avec sync.RWMutex. Validation des entrées.

Concepts clés :

  • net/http standard
  • gorilla/mux ou chi router
  • JSON encoding/decoding
  • Pagination, validation

Exercice 17 : Middleware Chain

Objectif : Comprendre et implémenter des middlewares.

Énoncé : Implémentez une chaîne de middlewares :

  1. LoggingMiddleware : log méthode, path, durée
  2. AuthMiddleware : vérifie un token JWT simple (HMAC)
  3. RateLimitMiddleware : limite par IP (token bucket)
  4. RecoveryMiddleware : récupération des panics
  5. CORSMiddleware : en-têtes CORS

Le tout doit être chainable : r.Use(m1, m2, m3, m4, m5)

Concepts clés :

  • func(http.Handler) http.Handler pattern
  • http.Handler interface
  • responseWriter wrapper pour capturer le status
  • Composition de middlewares

Exercice 18 : Graceful Shutdown

Objectif : Arrêt gracieux d'un serveur HTTP.

Énoncé : Implémentez un serveur HTTP avec :

  • Signal handling (SIGINT, SIGTERM)
  • shutdown avec timeout (30s max)
  • Drain des connexions actives
  • Context propagation aux handlers longs

Concepts clés :

  • os/signal package
  • http.Server.Shutdown()
  • context.WithTimeout
  • Connection draining

Exercice 19 : REST avec chi et validation

Objectif : API REST professionnelle avec chi.

Énoncé : Utilisez chi (ou gorilla/mux) pour créer une API avec :

  • go-playground/validator pour la validation
  • httpin pour le parsing des query params
  • slog pour le logging structuré
  • chi.middleware built-ins (Logger, Recoverer, RealIP, RequestID, Timeout)

Concepts clés :

  • chi router
  • Struct validation with tags
  • Structured logging
  • Request/response decoders

Module 5 : Database (Exercices 20-23)

Exercice 20 : PostgreSQL CRUD

Objectif : Opérations CRUD avec PostgreSQL et pgx.

Énoncé : Avec pgx (ou database/sql + lib/pq), implémentez :

type UserRepository interface {
    Create(ctx context.Context, user *User) error
    GetByID(ctx context.Context, id uuid.UUID) (*User, error)
    List(ctx context.Context, limit, offset int) ([]User, error)
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id uuid.UUID) error
}

Avec des migrations SQL (golang-migrate).

Concepts clés :

  • pgx ou database/sql
  • SQL migrations
  • uuid generation
  • Prepared statements

Exercice 21 : Redis Caching

Objectif : Cache Redis avec patterns avancés.

Énoncé : Implémentez un cache Redis avec :

  • Set/Get avec expiration TTL
  • Pattern Cache-Aside
  • Pattern de cache invalidation
  • Rate limiting (Redis)
  • Distributed locks avec SETNX

Concepts clés :

  • go-redis/redis client
  • SET, GET, EXPIRE
  • SETNX pour les locks
  • Pipeline Redis pour batch

Exercice 22 : Repository Pattern

Objectif : Implémenter le pattern repository avec des tests.

Énoncé : Créez une interface UserRepository avec deux implémentations :

  1. postgresRepository (PostgreSQL)
  2. inMemoryRepository (utilisée pour les tests)

Le but est de tester la couche service sans base de données.

Concepts clés :

  • Interface-based repository
  • Dependency injection
  • In-memory test implementation
  • go-sqlmock pour les tests SQL

Exercice 23 : Transaction Management

Objectif : Transactions PostgreSQL complexes.

Énoncé : Implémentez un transfert bancaire entre deux comptes :

  1. Débiter le compte A
  2. Créditer le compte B
  3. Enregistrer la transaction dans un journal
  4. Rollback si une étape échoue

Avec isolation niveau SERIALIZABLE.

Concepts clés :

  • BEGIN / COMMIT / ROLLBACK
  • Transaction isolation levels
  • pgx.Tx / database/sql.Tx
  • Deadlock detection

Module 6 : Testing (Exercices 24-27)

Exercice 24 : Table-Driven Tests

Objectif : Tests paramétrés en Go.

Énoncé : Testez la fonction CalculateTax(amount float64, rate float64) (float64, error) avec des table-driven tests couvrant :

  • Cas normaux (taux 0%, 5.5%, 20%)
  • Cas limites (0, montant négatif, taux négatif)
  • Cas de précision flottante
  • Sous-tests avec t.Run()

Concepts clés :

  • testing.T
  • Table-driven tests
  • t.Run pour sous-tests
  • t.Parallel

Exercice 25 : Mocking avec testify

Objectif : Mocking d'interfaces.

Énoncé : Avec testify/mock, mockez l'interface EmailSender :

type EmailSender interface {
    Send(to, subject, body string) error
}

Testez un NotificationService qui utilise EmailSender sans envoyer de vrais emails.

Concepts clés :

  • testify/mock
  • Interface mocking
  • testify/assert / testify/require
  • gomock (alternative)

Exercice 26 : Integration Tests

Objectif : Tests d'intégration avec testcontainers.

Énoncé : Avec testcontainers-go, créez des tests d'intégration qui :

  1. Lancen un container PostgreSQL
  2. Exécutent les migrations
  3. Insèrent des données de test
  4. Testent les opérations CRUD
  5. Nettoient le container après le test

Concepts clés :

  • testcontainers-go
  • testing.M pour setup/teardown global
  • Build tags (//go:build integration)
  • Docker lifecycle management

Exercice 27 : Fuzzing

Objectif : Tests de fuzzing.

Énoncé : Écrivez un test de fuzzing pour la fonction ParsePhoneNumber(s string) (PhoneNumber, error) qui parse un numéro de téléphone au format international.

Le fuzzer doit découvrir des cas cassant le parsing.

Concepts clés :

  • testing.F / f.Fuzz
  • Corpus de seed
  • go test -fuzz
  • Edge cases in string parsing

Module 7 : CLI avec Cobra (Exercices 28-30)

Exercice 28 : CLI Todo List

Objectif : Première application CLI.

Énoncé : Créez une todo-list CLI avec :

  • todo add "task description"
  • todo list [--all] [--done]
  • todo done <id>
  • todo delete <id>
  • Stockage JSON dans ~/.todo/tasks.json

Concepts clés :

  • cobra.Command
  • Persistent flags
  • JSON file storage
  • os/user pour le chemin home

Exercice 29 : CLI with Config

Objectif : CLI lisant une configuration.

Énoncé : Créez une CLI qui lit sa configuration depuis :

  1. Flags CLI
  2. Fichier YAML (~/.app/config.yaml)
  3. Variables d'environnement (préfixe APP_) Avec viper pour la gestion de configuration.

Concepts clés :

  • viper library
  • Configuration precedence
  • YAML parsing
  • Env variable binding

Exercice 30 : Multi-Command CLI

Objectif : CLI complexe avec sous-commandes.

Énoncé : Créez un CLI de gestion de projet avec :

  • project init <name> : initialise un projet
  • project build [--output] [--tags] : build le projet
  • project deploy --env <env> : déploie le projet
  • project version : affiche la version
  • Hooks pre/post exécution

Concepts clés :

  • Command nesting
  • PersistentRun / PreRun
  • Viper config binding
  • Cobra CLI best practices

Module 8 : gRPC (Exercices 31-33)

Exercice 31 : Unary gRPC

Objectif : Service gRPC unary.

Énoncé : Définissez un service UserService en protobuf :

service UserService {
    rpc CreateUser (CreateUserRequest) returns (User);
    rpc GetUser (GetUserRequest) returns (User);
    rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}

Implémentez le serveur et le client.

Concepts clés :

  • Protobuf definition
  • protoc generation
  • gRPC server setup
  • gRPC client

Exercice 32 : Server Streaming

Objectif : Streaming côté serveur.

Énoncé : Créez un service LogService avec un endpoint :

rpc StreamLogs(StreamLogsRequest) returns (stream LogEntry);

Le serveur stream les logs en temps réel. Le client les affiche au fur et à mesure.

Concepts clés :

  • Server-side streaming
  • grpc.ServerStream
  • Context cancellation
  • Backpressure

Exercice 33 : Bidirectional Streaming

Objectif : Streaming bidirectionnel (chat).

Énoncé : Implémentez un chat via gRPC streaming bidirectionnel :

service ChatService {
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

Chaque client envoie et reçoit des messages simultanément.

Concepts clés :

  • Bidirectional streaming
  • grpc.ClientStream
  • Goroutine management
  • Broadcast pattern

Module 9 : Generics (Exercices 34-36)

Exercice 34 : Generic Stack

Objectif : Pile générique thread-safe.

Énoncé : Implémentez une pile générique thread-safe :

type Stack[T any] struct { ... }
func New[T any]() *Stack[T]
func (s *Stack[T]) Push(item T)
func (s *Stack[T]) Pop() (T, bool)
func (s *Stack[T]) Peek() (T, bool)
func (s *Stack[T]) Len() int
func (s *Stack[T]) IsEmpty() bool

Concepts clés :

  • Type parameters
  • sync.RWMutex
  • Zero value of T
  • Generic constraints

Exercice 35 : Generic Map/Reduce/Filter

Objectif : Higher-order functions génériques.

Énoncé : Implémentez :

func Map[T, U any](s []T, f func(T) U) []U
func Filter[T any](s []T, f func(T) bool) []T
func Reduce[T, U any](s []T, init U, f func(U, T) U) U

Avec des tests pour différents types (int, string, struct).

Concepts clés :

  • Multiple type parameters
  • Generic functions
  • Functional programming in Go
  • Type inference

Exercice 36 : Generic Cache

Objectif : Cache typé avec expiration.

Énoncé : Implémentez un cache générique avec TTL :

type Cache[K comparable, V any] struct { ... }
func NewCache[K comparable, V any](ttl time.Duration, cleanupInterval time.Duration) *Cache[K, V]
func (c *Cache[K, V]) Get(key K) (V, bool)
func (c *Cache[K, V]) Set(key K, value V)
func (c *Cache[K, V]) Delete(key K)
func (c *Cache[K, V]) Len() int
func (c *Cache[K, V]) Clear()

Concepts clés :

  • comparable constraint
  • Generic + concurrency
  • TTL with background cleanup
  • Memory management

Module 10 : Profiling (Exercices 37-38)

Exercice 37 : CPU/Memory Profiling

Objectif : Profiler et optimiser une application.

Énoncé :

  1. Créez une application qui alloue massivement (JSON parsing, string concatenation)
  2. Profilez le CPU : go test -cpuprofile cpu.prof -bench .
  3. Profilez la mémoire : go test -memprofile mem.prof -bench .
  4. Analysez avec go tool pprof
  5. Optimisez les hotspots identifiés

Concepts clés :

  • pprof package
  • go tool pprof (text, graph, flamegraph)
  • Benchmark with profiling
  • Escape analysis
  • Memory allocation optimization

Exercice 38 : Trace Execution

Objectif : Tracer l'exécution concurrente.

Énoncé :

  1. Créez une application concurrente (worker pool ou pipeline)
  2. Générez une trace : go test -trace trace.out
  3. Analysez avec go tool trace
  4. Identifiez les goulets d'étranglement, les goroutines bloquées, les problèmes de scheduling

Concepts clés :

  • runtime/trace package
  • go tool trace
  • Goroutine scheduling
  • Blocking profiles
  • Network contention

Module 11 : Docker / Kubernetes (Exercices 39-40)

Exercice 39 : Multi-stage Docker Build

Objectif : Dockerfile multi-stage optimisé.

Énoncé : Créez un Dockerfile multi-stage pour une application Go :

  • Stage 1 : Build avec golang:1.23-alpine, cache des dépendances, build statique
  • Stage 2 : Image scratch minimaliste
  • Stage 3 (optionnel) : Image distroless pour debugging

Optimisations : CGO_ENABLED=0, -ldflags="-s -w", UPX compression.

Concepts clés :

  • Multi-stage builds
  • scratch / distroless images
  • Build args
  • Layer caching optimization
  • Security (non-root user)

Exercice 40 : Deploy on Kubernetes

Objectif : Déploiement Kubernetes complet.

Énoncé : Créez les manifests Kubernetes pour l'API de l'exercice 16 :

  • Deployment (3 replicas, rolling update)
  • Service (ClusterIP)
  • ConfigMap (configuration)
  • HorizontalPodAutoscaler (CPU > 70%)
  • Readiness + Liveness probes
  • Resource limits

Concepts clés :

  • Kubernetes manifests YAML
  • kubectl apply
  • Rolling updates
  • Probes (readiness, liveness)
  • HPA (Horizontal Pod Autoscaler)

Conseils généraux

  1. Lisez la spec attentivement avant de coder
  2. Écrivez les tests en premier (TDD) quand c'est approprié
  3. Utilisez go vet et golangci-lint après chaque exercice
  4. Ne regardez PAS le corrigé avant d'avoir terminé
  5. Comparez votre solution avec le corrigé pour apprendre
  6. Exécutez les benchmarks pour les exercices de performance

Références