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
jobspour 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 bouclerangeproprement
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

