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¶
# 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.