Aller au contenu

Exemples d'implémentation

Cette section présente deux projets complets et fonctionnels : une API REST CRUD avec Gin et GORM (persistence SQLite), puis un outil CLI avec Cobra integrant un worker pool concurrent. Les deux exemples sont écrits en Go 1.22+ et suivent les conventions de production.


Projet 1 — API REST CRUD avec Gin et GORM

Structure du projet

api/
├── main.go          # Point d'entree, configuration du serveur
├── models.go        # Struct Item + setup GORM
├── handlers.go      # Handlers Gin CRUD
└── go.mod

go.mod

module example.com/items-api

go 1.22

require (
    github.com/gin-gonic/gin v1.10.0
    gorm.io/driver/sqlite v1.5.5
    gorm.io/gorm v1.25.10
)

models.go — Struct et persistance GORM

package main

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

// Item est le modele GORM. Les tags `binding` valident le JSON entrant (Gin).
type Item struct {
    gorm.Model              // Ajoute ID, CreatedAt, UpdatedAt, DeletedAt
    Name  string `json:"name"  gorm:"not null"        binding:"required,min=1,max=200"`
    Price float64 `json:"price" gorm:"not null"       binding:"required,gt=0"`
    Stock int     `json:"stock" gorm:"default:0"      binding:"min=0"`
}

// ItemUpdate est utilise pour les mises a jour partielles (tous les champs optionnels).
type ItemUpdate struct {
    Name  *string  `json:"name"  binding:"omitempty,min=1,max=200"`
    Price *float64 `json:"price" binding:"omitempty,gt=0"`
    Stock *int     `json:"stock" binding:"omitempty,min=0"`
}

// SetupDB ouvre la base SQLite et migre le schema.
func SetupDB() (*gorm.DB, error) {
    db, err := gorm.Open(sqlite.Open("items.db"), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    // AutoMigrate cree ou met a jour la table items
    if err := db.AutoMigrate(&Item{}); err != nil {
        return nil, err
    }
    return db, nil
}

handlers.go — Handlers CRUD

package main

import (
    "errors"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

// Handler regroupe la reference a la base pour eviter les variables globales.
type Handler struct {
    DB *gorm.DB
}

// ListItems — GET /items?name=<filtre>
func (h *Handler) ListItems(c *gin.Context) {
    var items []Item
    query := h.DB

    // Filtre optionnel par nom (recherche partielle)
    if name := c.Query("name"); name != "" {
        query = query.Where("name LIKE ?", "%"+name+"%")
    }

    if err := query.Find(&items).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur base de donnees"})
        return
    }
    c.JSON(http.StatusOK, items)
}

// CreateItem — POST /items
func (h *Handler) CreateItem(c *gin.Context) {
    var item Item
    // ShouldBindJSON deserialise et valide via les tags `binding`
    if err := c.ShouldBindJSON(&item); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    if err := h.DB.Create(&item).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Echec creation"})
        return
    }
    c.JSON(http.StatusCreated, item)
}

// GetItem — GET /items/:id
func (h *Handler) GetItem(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
        return
    }

    var item Item
    if err := h.DB.First(&item, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            c.JSON(http.StatusNotFound, gin.H{"error": "Item non trouve"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur base de donnees"})
        return
    }
    c.JSON(http.StatusOK, item)
}

// UpdateItem — PUT /items/:id (mise a jour partielle)
func (h *Handler) UpdateItem(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
        return
    }

    var item Item
    if err := h.DB.First(&item, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            c.JSON(http.StatusNotFound, gin.H{"error": "Item non trouve"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur base de donnees"})
        return
    }

    var updates ItemUpdate
    if err := c.ShouldBindJSON(&updates); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Applique uniquement les champs non-nil (mise a jour partielle)
    if updates.Name != nil {
        item.Name = *updates.Name
    }
    if updates.Price != nil {
        item.Price = *updates.Price
    }
    if updates.Stock != nil {
        item.Stock = *updates.Stock
    }

    if err := h.DB.Save(&item).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Echec mise a jour"})
        return
    }
    c.JSON(http.StatusOK, item)
}

// DeleteItem — DELETE /items/:id
func (h *Handler) DeleteItem(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
        return
    }

    result := h.DB.Delete(&Item{}, id)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur base de donnees"})
        return
    }
    if result.RowsAffected == 0 {
        c.JSON(http.StatusNotFound, gin.H{"error": "Item non trouve"})
        return
    }
    c.Status(http.StatusNoContent)
}

main.go — Assemblage

package main

import (
    "log"

    "github.com/gin-gonic/gin"
)

func main() {
    db, err := SetupDB()
    if err != nil {
        log.Fatalf("Impossible d'ouvrir la base de donnees : %v", err)
    }

    h := &Handler{DB: db}

    r := gin.Default()
    items := r.Group("/items")
    {
        items.GET("", h.ListItems)
        items.POST("", h.CreateItem)
        items.GET("/:id", h.GetItem)
        items.PUT("/:id", h.UpdateItem)
        items.DELETE("/:id", h.DeleteItem)
    }

    log.Println("API demarree sur http://localhost:8080")
    log.Fatal(r.Run(":8080"))
}
# Lancement
go run .

# Test rapide
curl -X POST http://localhost:8080/items \
  -H "Content-Type: application/json" \
  -d '{"name":"Widget","price":9.99,"stock":100}'

Projet 2 — CLI avec Cobra et worker pool concurrent

Ce second projet illustre un outil CLI multi-commandes avec un worker pool utilisant des goroutines et des channels pour traiter des URLs en parallèle.

Structure

cli/
├── main.go          # Point d'entree Cobra
├── cmd/
│   ├── root.go      # Commande racine
│   └── fetch.go     # Sous-commande fetch avec worker pool
└── go.mod

cmd/root.go

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "fetcher",
    Short: "Outil de telechargement concurrent de pages web",
    Long:  "Fetcher telecharge plusieurs URLs en parallele avec un worker pool configurable.",
}

// Execute est le point d'entree de toutes les commandes.
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

cmd/fetch.go — Worker pool avec goroutines et channels

package cmd

import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"

    "github.com/spf13/cobra"
)

// Result contient le resultat du telechargement d'une URL.
type Result struct {
    URL      string
    Size     int
    Duration time.Duration
    Err      error
}

// fetchCmd est la sous-commande "fetch".
var fetchCmd = &cobra.Command{
    Use:   "fetch [urls...]",
    Short: "Telecharge les URLs en parallele",
    Args:  cobra.MinimumNArgs(1),
    RunE:  runFetch,
}

var workers int

func init() {
    fetchCmd.Flags().IntVarP(&workers, "workers", "w", 4, "Nombre de workers paralleles")
    rootCmd.AddCommand(fetchCmd)
}

// runFetch orchestre le worker pool.
func runFetch(cmd *cobra.Command, args []string) error {
    urls := args

    // jobs : canal d'entree du worker pool
    jobs := make(chan string, len(urls))
    // results : canal de sortie des resultats
    results := make(chan Result, len(urls))

    // Lancement des workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            worker(jobs, results)
        }()
    }

    // Envoi de tous les jobs dans le canal
    for _, url := range urls {
        jobs <- url
    }
    close(jobs) // Signal aux workers qu'il n'y a plus de travail

    // Attendre que tous les workers aient termine, puis fermer results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collecte et affichage des resultats
    var totalSize int
    var errors []Result
    for r := range results {
        if r.Err != nil {
            fmt.Printf("ERREUR  %-50s %v\n", r.URL, r.Err)
            errors = append(errors, r)
        } else {
            fmt.Printf("OK      %-50s %6d KB  %v\n", r.URL, r.Size/1024, r.Duration.Round(time.Millisecond))
            totalSize += r.Size
        }
    }

    fmt.Printf("\n--- Bilan : %d URLs, %d erreurs, %d KB total ---\n",
        len(urls), len(errors), totalSize/1024)
    return nil
}

// worker lit les URLs depuis jobs et envoie les resultats dans results.
// Chaque worker est une goroutine independante.
func worker(jobs <-chan string, results chan<- Result) {
    client := &http.Client{Timeout: 10 * time.Second}

    for url := range jobs {
        start := time.Now()
        size, err := downloadURL(client, url)
        results <- Result{
            URL:      url,
            Size:     size,
            Duration: time.Since(start),
            Err:      err,
        }
    }
}

// downloadURL telecharge une URL et retourne la taille du corps de la reponse.
func downloadURL(client *http.Client, url string) (int, error) {
    resp, err := client.Get(url)
    if err != nil {
        return 0, fmt.Errorf("requete echouee : %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return 0, fmt.Errorf("lecture du corps echouee : %w", err)
    }
    return len(body), nil
}

main.go

package main

import "example.com/fetcher/cmd"

func main() {
    cmd.Execute()
}
# Compilation et utilisation
go build -o fetcher .

# Telechargement de 3 URLs avec 2 workers
./fetcher fetch --workers 2 \
  https://go.dev \
  https://pkg.go.dev \
  https://github.com

# Affichage typique
# OK      https://go.dev                                       42 KB  213ms
# OK      https://pkg.go.dev                                   87 KB  341ms
# OK      https://github.com                                  215 KB  512ms
#
# --- Bilan : 3 URLs, 0 erreurs, 344 KB total ---

Pattern worker pool

Ce pattern — channel de jobs, channel de résultats, WaitGroup — est le worker pool idiomatique en Go. Il est preferable aux semaphores ou aux thread pools d'autres langages car il exprime clairement le flux de données. La fermeture de jobs sert de signal de fin : les workers sortent de range jobs proprement.