Chapitre 19
19 - Corrections
> Corrigés détaillés des 40 exercices du chapitre 17, avec code Go complet, explications, et analyses.
Cours 19 : Corrigés détaillés des 40 exercices
300+ lignes de corrigés avec code Go complet.
Exercice 01 : FizzBuzz
Analyse
L'exercice demande une fonction FizzBuzz avec gestion d'erreur. Les cas d'erreur incluent les nombres négatifs, nuls, et les overflow potentiels. La fonction doit retourner une error descriptive.
Code complet
package main
import (
"fmt"
"math"
)
func FizzBuzz(n int) (string, error) {
if n < 0 {
return "", fmt.Errorf("negative number: %d", n)
}
if n == 0 {
return "", fmt.Errorf("zero is not allowed")
}
if n > math.MaxInt32 {
return "", fmt.Errorf("number too large: %d", n)
}
switch {
case n%15 == 0:
return "FizzBuzz", nil
case n%3 == 0:
return "Fizz", nil
case n%5 == 0:
return "Buzz", nil
default:
return fmt.Sprintf("%d", n), nil
}
}
Tests
func TestFizzBuzz(t *testing.T) {
tests := []struct {
name string
n int
want string
err bool
}{
{"divisible by 3", 3, "Fizz", false},
{"divisible by 5", 5, "Buzz", false},
{"divisible by 15", 15, "FizzBuzz", false},
{"not divisible", 7, "7", false},
{"negative", -1, "", true},
{"zero", 0, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FizzBuzz(tt.n)
if (err != nil) != tt.err {
t.Errorf("FizzBuzz(%d) error = %v, wantErr %v", tt.n, err, tt.err)
}
if got != tt.want {
t.Errorf("FizzBuzz(%d) = %v, want %v", tt.n, got, tt.want)
}
})
}
}
Erreurs fréquentes
- Oublier le cas
n%15 == 0(testé en premier car 15 est divisible par 3 et 5) - Utiliser
else ifau lieu deswitch - Ne pas gérer l'erreur pour
n == 0
Exercice 02 : Palindrome Unicode
Analyse
La difficulté est la gestion de l'Unicode. Les chaînes Go sont en UTF-8. Il faut ignorer la casse et les caractères non-lettres.
Code complet
package main
import (
"strings"
"unicode"
)
func IsPalindrome(s string) bool {
clean := strings.Map(func(r rune) rune {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return -1
}
return unicode.ToLower(r)
}, s)
runes := []rune(clean)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
if runes[i] != runes[j] {
return false
}
}
return true
}
Tests
func TestIsPalindrome(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"A man, a plan, a canal: Panama", true},
{"racecar", true},
{"hello", false},
{"été", true},
{"世界", true}, // palindrome?
{"世界界世", true},
{"", true},
{"a", true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
if got := IsPalindrome(tt.input); got != tt.want {
t.Errorf("IsPalindrome(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
Points clés
strings.Mapavec retour-1pour supprimer un caractère- Conversion en
[]runepour itérer correctement sur l'Unicode - Comparaison depuis les extrémités vers le centre
Exercice 03 : Shape Interface
Analyse
Implémentation d'une interface et de plusieurs types concrets. Utilisation du duck typing et composition.
Code complet
package main
import (
"fmt"
"math"
"sort"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Triangle struct {
A, B, C float64
}
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 {
return t.A + t.B + t.C
}
func TotalArea(shapes ...Shape) float64 {
var total float64
for _, s := range shapes {
total += s.Area()
}
return total
}
// Tri par aire
type ByArea []Shape
func (a ByArea) Len() int { return len(a) }
func (a ByArea) Less(i, j int) bool { return a[i].Area() < a[j].Area() }
func (a ByArea) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
Triangle{A: 3, B: 4, C: 5},
}
fmt.Printf("Total area: %.2f\n", TotalArea(shapes...))
sort.Sort(ByArea(shapes))
for _, s := range shapes {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
}
Exercice 04 : JSON Processing
Code complet
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Emails []string `json:"emails,omitempty"`
Address *Address `json:"address,omitempty"`
}
func (p Person) MarshalJSON() ([]byte, error) {
type Alias Person
return json.Marshal(&struct {
Age string `json:"age"`
*Alias
}{
Age: fmt.Sprintf("%d years", p.Age),
Alias: (*Alias)(&p),
})
}
func (p *Person) UnmarshalJSON(data []byte) error {
type Alias Person
aux := &struct {
Age string `json:"age"`
*Alias
}{
Alias: (*Alias)(p),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
// Parse age from "XX years" format - simplified
fmt.Sscanf(aux.Age, "%d years", &p.Age)
return nil
}
Exercice 05 : Custom Sorting
Code complet
package main
import (
"fmt"
"math/rand"
"sort"
)
type Product struct {
Name string
Price float64
Rating float64
}
func main() {
products := []Product{
{"Laptop", 999.99, 4.5},
{"Mouse", 29.99, 4.8},
{"Keyboard", 79.99, 4.2},
{"Monitor", 299.99, 4.6},
}
// Tri par nom (ascendant)
sort.Slice(products, func(i, j int) bool {
return products[i].Name < products[j].Name
})
// Tri par prix (descendant)
sort.Slice(products, func(i, j int) bool {
return products[i].Price > products[j].Price
})
// Tri par rating (ascendant)
sort.Slice(products, func(i, j int) bool {
return products[i].Rating < products[j].Rating
})
// Tri composé : prix * rating
sort.Slice(products, func(i, j int) bool {
scoreI := products[i].Price * products[i].Rating
scoreJ := products[j].Price * products[j].Rating
return scoreI > scoreJ
})
}
Exercice 06 : Worker Pool
Code complet
package main
import (
"context"
"fmt"
"sync"
)
type Job func(ctx context.Context) (interface{}, error)
type Result struct {
Value interface{}
Err error
}
type Pool struct {
jobs chan Job
results chan Result
wg sync.WaitGroup
}
func NewPool(ctx context.Context, workers int) *Pool {
p := &Pool{
jobs: make(chan Job),
results: make(chan Result),
}
for i := 0; i < workers; i++ {
p.wg.Add(1)
go p.worker(ctx, i)
}
return p
}
func (p *Pool) worker(ctx context.Context, id int) {
defer p.wg.Done()
for {
select {
case job, ok := <-p.jobs:
if !ok {
return
}
value, err := job(ctx)
p.results <- Result{Value: value, Err: err}
case <-ctx.Done():
return
}
}
}
func (p *Pool) Submit(job Job) {
p.jobs <- job
}
func (p *Pool) Results() <-chan Result {
return p.results
}
func (p *Pool) Wait() {
close(p.jobs)
p.wg.Wait()
close(p.results)
}
Exercice 07 : Pipeline
Code complet
package main
import "fmt"
func Generate(numbers ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range numbers {
out <- n
}
close(out)
}()
return out
}
func MultiplyBy2(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * 2
}
close(out)
}()
return out
}
func FilterEven(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 != 0 {
out <- n
}
}
close(out)
}()
return out
}
func main() {
numbers := Generate(1, 2, 3, 4, 5, 6)
multiplied := MultiplyBy2(numbers)
filtered := FilterEven(multiplied)
for result := range filtered {
fmt.Println(result)
}
}
Exercice 08 : Rate Limiter
Code complet
package main
import (
"context"
"time"
)
type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
}
func NewRateLimiter(rate int, burst int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, burst),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
}
// Remplir le bucket initial
for i := 0; i < burst; i++ {
rl.tokens <- struct{}{}
}
go func() {
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
// Bucket plein
}
}
}()
return rl
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}
func (rl *RateLimiter) Wait(ctx context.Context) error {
select {
case <-rl.tokens:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (rl *RateLimiter) Stop() {
rl.ticker.Stop()
}
Exercice 09 : Timeout avec Context
Code complet
package main
import (
"context"
"fmt"
"time"
)
func FetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
resultCh := make(chan string, 1)
errCh := make(chan error, 1)
go func() {
// Simulation d'un appel HTTP long
select {
case <-time.After(2 * time.Second):
resultCh <- fmt.Sprintf("response from %s", url)
case <-ctx.Done():
errCh <- ctx.Err()
}
}()
select {
case result := <-resultCh:
return result, nil
case err := <-errCh:
return "", err
case <-ctx.Done():
return "", ctx.Err()
}
}
Exercice 10 : Merge Channels
Code complet
package main
import (
"sync"
)
// Approche 1 : avec sync.WaitGroup
func Merge(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
// Approche 2 : avec reflect.Select (dynamique)
func MergeReflect(channels ...<-chan int) <-chan int {
// Implémentation avec reflect.Select pour le cas où
// le nombre de channels est connu dynamiquement
out := make(chan int)
go func() {
defer close(out)
cases := make([]reflect.SelectCase, len(channels))
for i, ch := range channels {
cases[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ch),
}
}
for len(cases) > 0 {
i, v, ok := reflect.Select(cases)
if !ok {
cases = append(cases[:i], cases[i+1:]...)
continue
}
out <- int(v.Int())
}
}()
return out
}
Exercice 11 : Prime Sieve
Code complet
package main
import "fmt"
func Generate(ch chan<- int) {
for i := 2; ; i++ {
ch <- i
}
}
func Filter(in <-chan int, out chan<- int, prime int) {
for {
n := <-in
if n%prime != 0 {
out <- n
}
}
}
func Sieve(limit int) []int {
ch := make(chan int)
go Generate(ch)
var primes []int
for i := 0; i < limit; i++ {
prime := <-ch
primes = append(primes, prime)
ch1 := make(chan int)
go Filter(ch, ch1, prime)
ch = ch1
}
return primes
}
func main() {
primes := Sieve(20)
fmt.Println(primes)
}
Exercice 12 : Pub/Sub
Code complet
package main
import (
"sync"
)
type PubSub struct {
mu sync.RWMutex
subs map[string][]chan interface{}
closed bool
}
func NewPubSub() *PubSub {
return &PubSub{
subs: make(map[string][]chan interface{}),
}
}
func (ps *PubSub) Subscribe(topic string) <-chan interface{} {
ps.mu.Lock()
defer ps.mu.Unlock()
ch := make(chan interface{}, 1)
ps.subs[topic] = append(ps.subs[topic], ch)
return ch
}
func (ps *PubSub) Publish(topic string, msg interface{}) {
ps.mu.RLock()
defer ps.mu.RUnlock()
if ps.closed {
return
}
for _, ch := range ps.subs[topic] {
ch <- msg
}
}
func (ps *PubSub) Unsubscribe(topic string, ch <-chan interface{}) {
ps.mu.Lock()
defer ps.mu.Unlock()
subs := ps.subs[topic]
for i, sub := range subs {
if sub == ch {
ps.subs[topic] = append(subs[:i], subs[i+1:]...)
close(sub)
break
}
}
}
func (ps *PubSub) Close() {
ps.mu.Lock()
defer ps.mu.Unlock()
if ps.closed {
return
}
ps.closed = true
for _, subs := range ps.subs {
for _, ch := range subs {
close(ch)
}
}
}
Exercice 13 : Circuit Breaker
Code complet
package main
import (
"errors"
"sync"
"time"
)
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
mu sync.Mutex
state State
failures int
maxFailures int
resetTimeout time.Duration
lastFailure time.Time
}
func NewCircuitBreaker(maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
maxFailures: maxFailures,
resetTimeout: resetTimeout,
}
}
func (cb *CircuitBreaker) Execute(fn func() error) error {
cb.mu.Lock()
switch cb.state {
case StateOpen:
if time.Since(cb.lastFailure) > cb.resetTimeout {
cb.state = StateHalfOpen
} else {
cb.mu.Unlock()
return errors.New("circuit breaker is open")
}
case StateHalfOpen:
// Allow one request through
}
cb.mu.Unlock()
err := fn()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = StateOpen
}
return err
}
// Success
cb.failures = 0
if cb.state == StateHalfOpen {
cb.state = StateClosed
}
return nil
}
Exercice 14 : Fan-Out/Fan-In
Code complet
package main
import (
"fmt"
"sync"
)
func FanOut(in <-chan string, workers int) []<-chan string {
channels := make([]<-chan string, workers)
for i := 0; i < workers; i++ {
ch := make(chan string)
channels[i] = ch
go func(out chan<- string, id int) {
for word := range in {
out <- fmt.Sprintf("worker %d processed: %s (len=%d)", id, word, len(word))
}
close(out)
}(ch, i)
}
return channels
}
func FanIn(channels ...<-chan string) <-chan string {
out := make(chan string)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan string) {
defer wg.Done()
for msg := range c {
out <- msg
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Exercice 15 : Pipeline with Error Handling
Code complet
package main
import (
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func Step1(in <-chan int) (<-chan int, <-chan error) {
out := make(chan int)
errCh := make(chan error, 1)
go func() {
defer close(out)
defer close(errCh)
for n := range in {
if n < 0 {
errCh <- fmt.Errorf("negative number: %d", n)
return
}
out <- n * 2
}
}()
return out, errCh
}
func retry(fn func() error, attempts int) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(); err == nil {
return nil
}
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
}
return fmt.Errorf("failed after %d attempts: %w", attempts, err)
}
func main() {
var g errgroup.Group
numbers := []int{1, 2, 3, 4, -5, 6}
for _, n := range numbers {
n := n
g.Go(func() error {
return retry(func() error {
if n < 0 {
return fmt.Errorf("invalid: %d", n)
}
return nil
}, 3)
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Pipeline errors: %v\n", err)
}
}
Exercice 16 : REST API CRUD
Code complet
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type BookStore struct {
mu sync.RWMutex
books map[string]*Book
}
func NewBookStore() *BookStore {
return &BookStore{books: make(map[string]*Book)}
}
func (s *BookStore) Create(book *Book) {
s.mu.Lock()
defer s.mu.Unlock()
book.ID = uuid.New().String()
book.CreatedAt = time.Now()
book.UpdatedAt = time.Now()
s.books[book.ID] = book
}
func (s *BookStore) Get(id string) (*Book, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
book, ok := s.books[id]
return book, ok
}
func (s *BookStore) List() []*Book {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]*Book, 0, len(s.books))
for _, book := range s.books {
result = append(result, book)
}
return result
}
func (s *BookStore) Update(book *Book) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.books[book.ID]; !ok {
return false
}
book.UpdatedAt = time.Now()
s.books[book.ID] = book
return true
}
func (s *BookStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.books[id]; !ok {
return false
}
delete(s.books, id)
return true
}
type BookHandler struct {
store *BookStore
}
func (h *BookHandler) Create(w http.ResponseWriter, r *http.Request) {
var book Book
if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
h.store.Create(&book)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(book)
}
func (h *BookHandler) Get(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
book, ok := h.store.Get(id)
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(book)
}
func (h *BookHandler) List(w http.ResponseWriter, r *http.Request) {
books := h.store.List()
json.NewEncoder(w).Encode(books)
}
func (h *BookHandler) Update(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var book Book
if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
book.ID = id
if !h.store.Update(&book) {
http.Error(w, "not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(book)
}
func (h *BookHandler) Delete(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
if !h.store.Delete(id) {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}
func main() {
store := NewBookStore()
handler := &BookHandler{store: store}
r := mux.NewRouter()
r.HandleFunc("/api/books", handler.List).Methods("GET")
r.HandleFunc("/api/books", handler.Create).Methods("POST")
r.HandleFunc("/api/books/{id}", handler.Get).Methods("GET")
r.HandleFunc("/api/books/{id}", handler.Update).Methods("PUT")
r.HandleFunc("/api/books/{id}", handler.Delete).Methods("DELETE")
log.Fatal(http.ListenAndServe(":8080", r))
}
Exercice 17 : Middleware Chain
Code complet
package main
import (
"log"
"net/http"
"time"
)
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, rw.status, time.Since(start))
})
}
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("panic recovered: %v", err)
}
}()
next.ServeHTTP(w, r)
})
}
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Exercice 18 : Graceful Shutdown
Code complet
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // Simulate long request
w.Write([]byte("done"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Channel for shutdown signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Printf("Server listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s", err)
}
}()
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("Server exited gracefully")
}
Exercice 19 : REST avec chi
Code complet
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-playground/validator/v10"
)
type CreateBookRequest struct {
Title string `json:"title" validate:"required,min=1,max=200"`
Author string `json:"author" validate:"required,min=2,max=100"`
ISBN string `json:"isbn" validate:"required,len=13"`
}
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
CreatedAt time.Time `json:"created_at"`
}
var validate = validator.New()
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RealIP)
r.Use(middleware.RequestID)
r.Use(middleware.Timeout(30 * time.Second))
r.Post("/api/books", func(w http.ResponseWriter, r *http.Request) {
var req CreateBookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validate.Struct(req); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(req)
})
log.Fatal(http.ListenAndServe(":8080", r))
}
Exercice 20 : PostgreSQL CRUD
Code complet
package main
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type UserRepository struct {
pool *pgxpool.Pool
}
func (r *UserRepository) Create(ctx context.Context, user *User) error {
user.ID = uuid.New()
user.CreatedAt = time.Now()
_, err := r.pool.Exec(ctx,
`INSERT INTO users (id, name, email, created_at) VALUES ($1, $2, $3, $4)`,
user.ID, user.Name, user.Email, user.CreatedAt,
)
return err
}
func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
user := &User{}
err := r.pool.QueryRow(ctx,
`SELECT id, name, email, created_at FROM users WHERE id = $1`, id,
).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return user, nil
}
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]User, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
limit, offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
Exercice 21 : Redis Caching
Code complet
package main
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type Cache struct {
client *redis.Client
ttl time.Duration
}
func NewCache(addr string, password string, db int, ttl time.Duration) *Cache {
return &Cache{
client: redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
}),
ttl: ttl,
}
}
func (c *Cache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, key).Bytes()
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
func (c *Cache) Set(ctx context.Context, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, c.ttl).Err()
}
func (c *Cache) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}
// Distributed lock
func (c *Cache) AcquireLock(ctx context.Context, key string, ttl time.Duration) (bool, error) {
return c.client.SetNX(ctx, key, "locked", ttl).Result()
}
func (c *Cache) ReleaseLock(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}
Exercice 22 : Repository Pattern
Code complet
package main
import (
"context"
"fmt"
"sync"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID
Name string
Email string
}
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
List(ctx context.Context) ([]User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
}
type InMemoryUserRepository struct {
mu sync.RWMutex
users map[uuid.UUID]*User
}
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{users: make(map[uuid.UUID]*User)}
}
func (r *InMemoryUserRepository) Create(ctx context.Context, user *User) error {
r.mu.Lock()
defer r.mu.Unlock()
user.ID = uuid.New()
r.users[user.ID] = user
return nil
}
func (r *InMemoryUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.users[id]
if !ok {
return nil, fmt.Errorf("user not found: %s", id)
}
return user, nil
}
func (r *InMemoryUserRepository) List(ctx context.Context) ([]User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]User, 0, len(r.users))
for _, u := range r.users {
users = append(users, *u)
}
return users, nil
}
func (r *InMemoryUserRepository) Update(ctx context.Context, user *User) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[user.ID]; !ok {
return fmt.Errorf("user not found: %s", user.ID)
}
r.users[user.ID] = user
return nil
}
func (r *InMemoryUserRepository) Delete(ctx context.Context, id uuid.UUID) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[id]; !ok {
return fmt.Errorf("user not found: %s", id)
}
delete(r.users, id)
return nil
}
Exercice 23 : Transaction Management
Code complet
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
type AccountService struct {
pool *pgxpool.Pool
}
func (s *AccountService) Transfer(ctx context.Context, fromID, toID string, amount float64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`,
amount, fromID,
)
if err != nil {
return fmt.Errorf("debit: %w", err)
}
_, err = tx.Exec(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`,
amount, toID,
)
if err != nil {
return fmt.Errorf("credit: %w", err)
}
_, err = tx.Exec(ctx,
`INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)`,
fromID, toID, amount,
)
if err != nil {
return fmt.Errorf("log transfer: %w", err)
}
return tx.Commit(ctx)
}
Exercice 24 : Table-Driven Tests
Code complet
package main
import (
"testing"
)
func CalculateTax(amount float64, rate float64) (float64, error) {
if amount < 0 {
return 0, fmt.Errorf("negative amount: %f", amount)
}
if rate < 0 || rate > 1 {
return 0, fmt.Errorf("invalid rate: %f", rate)
}
return amount * rate, nil
}
func TestCalculateTax(t *testing.T) {
tests := []struct {
name string
amount float64
rate float64
want float64
wantErr bool
}{
{"zero amount", 0, 0.2, 0, false},
{"standard rate", 100, 0.2, 20, false},
{"full rate", 100, 1, 100, false},
{"negative amount", -100, 0.2, 0, true},
{"negative rate", 100, -0.1, 0, true},
{"over 100% rate", 100, 1.5, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalculateTax(tt.amount, tt.rate)
if (err != nil) != tt.wantErr {
t.Errorf("CalculateTax() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CalculateTax() = %v, want %v", got, tt.want)
}
})
}
}
Exercice 25 : Mocking
Code complet
package main
import (
"testing"
"github.com/stretchr/testify/mock"
)
type EmailSender interface {
Send(to, subject, body string) error
}
type MockEmailSender struct {
mock.Mock
}
func (m *MockEmailSender) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
type NotificationService struct {
sender EmailSender
}
func (s *NotificationService) Notify(email, message string) error {
return s.sender.Send(email, "Notification", message)
}
func TestNotificationService_Notify(t *testing.T) {
mockSender := new(MockEmailSender)
service := &NotificationService{sender: mockSender}
mockSender.On("Send", "test@example.com", "Notification", "Hello").
Return(nil)
err := service.Notify("test@example.com", "Hello")
assert.NoError(t, err)
mockSender.AssertExpectations(t)
}
Exercice 26 : Integration Tests
Code complet
package main
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestPostgresIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_DB": "testdb",
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
},
WaitingFor: wait.ForLog("database system is ready"),
}
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
}
defer postgres.Terminate(ctx)
// Use the container in tests
// repo := NewUserRepository(postgres.GetConnectionString())
}
Exercice 27 : Fuzzing
Code complet
package main
import (
"errors"
"regexp"
"testing"
)
var phoneRegex = regexp.MustCompile(`^\+?(\d{1,3})?[-. ]?\(?\d{1,4}\)?[-. ]?\d{1,4}[-. ]?\d{1,9}$`)
func ParsePhoneNumber(s string) error {
if len(s) > 20 {
return errors.New("too long")
}
if !phoneRegex.MatchString(s) {
return errors.New("invalid format")
}
return nil
}
func FuzzParsePhoneNumber(f *testing.F) {
seedCorpora := []string{
"+1-555-123-4567",
"+33 6 12 34 56 78",
"1234567890",
"+1 (555) 123-4567",
}
for _, seed := range seedCorpora {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, input string) {
err := ParsePhoneNumber(input)
if err != nil {
t.Skip()
}
})
}
Exercice 28 : CLI Todo avec Cobra
Code complet
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
type Task struct {
ID int `json:"id"`
Done bool `json:"done"`
Text string `json:"text"`
}
type TodoList struct {
Tasks []Task `json:"tasks"`
NextID int `json:"next_id"`
}
var dataFile string
func loadTasks() (*TodoList, error) {
data, err := os.ReadFile(dataFile)
if err != nil {
if os.IsNotExist(err) {
return &TodoList{NextID: 1}, nil
}
return nil, err
}
var list TodoList
if err := json.Unmarshal(data, &list); err != nil {
return nil, err
}
return &list, nil
}
func saveTasks(list *TodoList) error {
data, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
return os.WriteFile(dataFile, data, 0644)
}
func main() {
home, _ := os.UserHomeDir()
dataFile = filepath.Join(home, ".todo", "tasks.json")
os.MkdirAll(filepath.Dir(dataFile), 0755)
var rootCmd = &cobra.Command{Use: "todo"}
var addCmd = &cobra.Command{
Use: "add [task]",
Short: "Add a new task",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
list, _ := loadTasks()
task := Task{ID: list.NextID, Text: args[0]}
list.Tasks = append(list.Tasks, task)
list.NextID++
saveTasks(list)
fmt.Printf("Added task %d: %s\n", task.ID, task.Text)
},
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List all tasks",
Run: func(cmd *cobra.Command, args []string) {
list, _ := loadTasks()
for _, t := range list.Tasks {
status := " "
if t.Done {
status = "✓"
}
fmt.Printf("[%s] %d: %s\n", status, t.ID, t.Text)
}
},
}
rootCmd.AddCommand(addCmd, listCmd)
rootCmd.Execute()
}
Exercice 29 : CLI with Config
Code complet
package main
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME/.app")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
viper.SetDefault("server.port", 8080)
viper.SetDefault("database.host", "localhost")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
log.Fatalf("error reading config: %s", err)
}
}
var rootCmd = &cobra.Command{Use: "app"}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the server",
Run: func(cmd *cobra.Command, args []string) {
port := viper.GetInt("server.port")
dbHost := viper.GetString("database.host")
fmt.Printf("Starting server on port %d (db: %s)\n", port, dbHost)
},
}
rootCmd.AddCommand(serveCmd)
rootCmd.Execute()
}
Exercice 30 : Multi-Command CLI
Code complet
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var outputDir string
var buildTags string
var environment string
func main() {
var rootCmd = &cobra.Command{
Use: "project",
Short: "Project management CLI",
}
var initCmd = &cobra.Command{
Use: "init [name]",
Short: "Initialize a new project",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Initializing project: %s\n", args[0])
},
}
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build the project",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Building with output=%s tags=%s\n", outputDir, buildTags)
},
}
buildCmd.Flags().StringVarP(&outputDir, "output", "o", "./dist", "Output directory")
buildCmd.Flags().StringVarP(&buildTags, "tags", "t", "", "Build tags")
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy the project",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Deploying to environment: %s\n", environment)
},
}
deployCmd.Flags().StringVarP(&environment, "env", "e", "dev", "Deployment environment")
deployCmd.MarkFlagRequired("env")
rootCmd.AddCommand(initCmd, buildCmd, deployCmd)
rootCmd.Execute()
}
Exercice 31 : gRPC Unary
Code complet
package main
// Proto definition:
// service UserService {
// rpc CreateUser (CreateUserRequest) returns (User);
// rpc GetUser (GetUserRequest) returns (User);
// rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
// }
//
// message User {
// string id = 1;
// string name = 2;
// string email = 3;
// }
// message CreateUserRequest { string name = 1; string email = 2; }
// message GetUserRequest { string id = 1; }
// message ListUsersRequest {}
// message ListUsersResponse { repeated User users = 1; }
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "path/to/proto"
)
type server struct {
pb.UnimplementedUserServiceServer
users map[string]*pb.User
}
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
user := &pb.User{
Id: uuid.New().String(),
Name: req.Name,
Email: req.Email,
}
s.users[user.Id] = user
return user, nil
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "user not found")
}
return user, nil
}
func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{users: make(map[string]*pb.User)})
reflection.Register(s)
log.Fatal(s.Serve(lis))
}
Exercice 32 : gRPC Server Streaming
Code complet
package main
import (
"log"
"net"
"time"
"google.golang.org/grpc"
pb "path/to/proto"
)
type LogServer struct {
pb.UnimplementedLogServiceServer
}
func (s *LogServer) StreamLogs(req *pb.StreamLogsRequest, stream pb.LogService_StreamLogsServer) error {
for i := 0; i < 100; i++ {
entry := &pb.LogEntry{
Timestamp: time.Now().Unix(),
Level: "INFO",
Message: fmt.Sprintf("log entry %d", i),
}
if err := stream.Send(entry); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
Exercice 33 : gRPC Bidirectional Streaming
Code complet
package main
import (
"io"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/proto"
)
type ChatServer struct {
pb.UnimplementedChatServiceServer
}
func (s *ChatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// Broadcast to all connected clients
stream.Send(&pb.ChatMessage{
User: msg.User,
Message: msg.Message,
})
}
}
Exercice 34 : Generic Stack
Code complet
package main
import "sync"
type Stack[T any] struct {
mu sync.RWMutex
items []T
}
func New[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
func (s *Stack[T]) IsEmpty() bool {
return s.Len() == 0
}
Exercice 35 : Generic Map/Reduce/Filter
Code complet
package main
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func Filter[T any](s []T, f func(T) bool) []T {
var result []T
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
result := init
for _, v := range s {
result = f(result, v)
}
return result
}
Exercice 36 : Generic Cache
Code complet
package main
import (
"sync"
"time"
)
type cacheItem[V any] struct {
value V
expiresAt time.Time
}
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]cacheItem[V]
ttl time.Duration
stopCh chan struct{}
}
func NewCache[K comparable, V any](ttl time.Duration, cleanupInterval time.Duration) *Cache[K, V] {
c := &Cache[K, V]{
items: make(map[K]cacheItem[V]),
ttl: ttl,
stopCh: make(chan struct{}),
}
go c.cleanup(cleanupInterval)
return c
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.expiresAt) {
var zero V
return zero, false
}
return item.value, true
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = cacheItem[V]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache[K, V]) cleanup(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.mu.Lock()
now := time.Now()
for k, v := range c.items {
if now.After(v.expiresAt) {
delete(c.items, k)
}
}
c.mu.Unlock()
case <-c.stopCh:
return
}
}
}
Exercice 37 : CPU/Memory Profiling
Code complet
package main
import (
"bytes"
"encoding/json"
"os"
"runtime/pprof"
)
type Data struct {
ID int `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
}
func generateData(n int) []Data {
data := make([]Data, n)
for i := 0; i < n; i++ {
data[i] = Data{
ID: i,
Name: "item-" + string(rune(i)),
Value: "value-" + string(rune(i)),
}
}
return data
}
func main() {
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
data := generateData(100000)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(data)
}
Exercice 38 : Trace Execution
Code complet
package main
import (
"context"
"os"
"runtime/trace"
"sync"
)
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
results <- job * 2
}
}
func main() {
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
ctx := context.Background()
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, i, jobs, results, &wg)
}
for i := 0; i < 100; i++ {
jobs <- i
}
close(jobs)
wg.Wait()
close(results)
}
Exercice 39 : Docker Multi-stage Build
Dockerfile
# Stage 1: Build
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Stage 2: Runtime
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]
Exercice 40 : Kubernetes Deployment
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-api
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: go-api
template:
metadata:
labels:
app: go-api
spec:
containers:
- name: api
image: myapp:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: go-api-config
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: go-api
spec:
selector:
app: go-api
ports:
- port: 80
targetPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: go-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: go-api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Ceci conclut les 40 corrigés d'exercices. Chaque solution met l'accent sur les idiomes Go, la gestion correcte des erreurs, et les bonnes pratiques. Consultez le chapitre 20 pour les ressources complémentaires.