En Go, Le worker est un pattern très simple à implémenter.

👉 une goroutine qui consomme des tâches depuis un channel jusqu’à sa fermeture.

🎯 Pourquoi un worker ?

Le problème classique : vous avez des tâches à traiter, et vous voulez les exécuter en arrière-plan sans bloquer votre programme principal.

Le principe : découpler la soumission du traitement.

Exemples :

  • Envoi d’emails — ne pas bloquer la requête HTTP pendant l’envoi
  • Logging asynchrone — écrire les logs sans ralentir le code métier
  • Traitement de webhooks — acquitter immédiatement, traiter après
  • Génération de rapports — lancer en fond, notifier quand c’est prêt

🛠️ L’implémentation minimale

package main

import (
    "fmt"
    "time"
)

func worker(jobs <-chan string) {
    for job := range jobs {
        fmt.Printf("Processing: %s\n", job)
        time.Sleep(100 * time.Millisecond) // simule un traitement
    }
    fmt.Println("Worker stopped")
}

func main() {
    jobs := make(chan string)

    // Lancer le worker
    go worker(jobs)

    // Envoyer des tâches
    jobs <- "task-1"
    jobs <- "task-2"
    jobs <- "task-3"

    // Signaler la fin
    close(jobs)

    // Attendre un peu pour voir la sortie
    time.Sleep(500 * time.Millisecond)
}

Ce qui se passe :

  • On crée un channel jobs pour envoyer du travail
  • On lance une goroutine qui consomme ce channel avec range
  • Chaque tâche est traitée séquentiellement par le worker
  • close(jobs) termine la boucle range proprement

Le tout en quelque 10 lignes pour un worker fonctionnel.

🔄 Version avec synchronisation

On remplace le time.Sleep par un WaitGroup : au lieu d’attendre une durée arbitraire, on attend la fin effective du worker.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(jobs <-chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Processing: %s\n", job)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    jobs := make(chan string)
    var wg sync.WaitGroup

    wg.Add(1)
    go worker(jobs, &wg)

    jobs <- "task-1"
    jobs <- "task-2"
    jobs <- "task-3"

    close(jobs)
    wg.Wait() // attend proprement la fin du worker
}

Le WaitGroup garantit qu’on attend la fin du traitement avant de quitter.

🚀 Cas d’usage : file d’emails

Voici comment implémenter une file d’envoi d’emails asynchrone :

type EmailJob struct {
    To      string
    Subject string
    Body    string
}

type EmailWorker struct {
    jobs   chan EmailJob
    wg     sync.WaitGroup
    sender EmailSender
}

func NewEmailWorker(sender EmailSender, bufferSize int) *EmailWorker {
    w := &EmailWorker{
        jobs:   make(chan EmailJob, bufferSize),
        sender: sender,
    }
    w.wg.Add(1)
    go w.run()
    return w
}

func (w *EmailWorker) run() {
    defer w.wg.Done()
    for job := range w.jobs {
        if err := w.sender.Send(job.To, job.Subject, job.Body); err != nil {
            log.Printf("Failed to send email to %s: %v", job.To, err)
        }
    }
}

func (w *EmailWorker) Enqueue(job EmailJob) {
    w.jobs <- job
}

func (w *EmailWorker) Shutdown() {
    close(w.jobs)
    w.wg.Wait()
}

Utilisation :

func main() {
    worker := NewEmailWorker(smtpSender, 100)
    defer worker.Shutdown()

    // Dans un handler HTTP
    http.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) {
        // ... créer l'utilisateur ...

        // Email envoyé en background, réponse immédiate
        worker.Enqueue(EmailJob{
            To:      user.Email,
            Subject: "Bienvenue !",
            Body:    "Votre compte est créé.",
        })

        w.WriteHeader(http.StatusCreated)
    })
}

Le handler HTTP répond immédiatement. L’email part en arrière-plan.

⚠️ Les pièges à éviter

1. Oublier de fermer le channel

// ❌ Le worker tourne indéfiniment
go worker(jobs)
// ... on n'appelle jamais close(jobs)
// Le programme leak une goroutine

Solution : Toujours avoir un owner clair qui appelle close().

2. Channel non bufferisé qui bloque

jobs := make(chan string) // unbuffered

// ❌ Bloque si le worker ne lit pas assez vite
jobs <- "task-1" // bloque jusqu'à ce que le worker lise

Solution : Utiliser un buffer si les producteurs ne doivent pas attendre.

jobs := make(chan string, 100) // buffered

3. Pas de gestion du contexte

En production, le worker doit s’arrêter proprement quand l’application s’arrête :

func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            return // arrêt propre
        case job, ok := <-jobs:
            if !ok {
                return
            }
            process(job)
        }
    }
}

📊 Quand un seul worker suffit

Un worker unique est parfait quand :

  • Le débit est faible — quelques tâches par seconde
  • L’ordre compte — les tâches doivent être traitées séquentiellement
  • La simplicité prime — pas besoin de complexité supplémentaire
  • Les ressources sont limitées — une seule connexion DB, un seul fichier

📝 En résumé

  • Pattern de base — Goroutine + channel + range = worker
  • Fermer le channel — C’est le signal d’arrêt
  • WaitGroup — Pour attendre proprement la fin
  • Buffer — Ajuster selon la pression producteur/consommateur
  • Context — Indispensable en production pour l’arrêt propre

Le worker simple, c’est 10 lignes pour la version de base, 30 pour une version production.
C’est la fondation sur laquelle tout le reste s’appuie.


Et si un seul worker ne suffit pas ? Quand le volume de tâches augmente ou que le traitement est lent, un worker unique devient un goulot d’étranglement.
La solution : le Worker Pool — plusieurs workers qui consomment la même file. C’est le sujet du prochain article.

Retrouver des exemples de code sur github