Motifs de concurrence Go : 7 motifs de production du Worker Pool au Pipeline
Quand les fuites de goroutines rencontrent la concurrence non bornée : un cauchemar de production
3h du matin, alerte OOM en production. L'investigation révèle : un gestionnaire HTTP lançant des goroutines sans limite — 3 goroutines par requête, QPS 1000 signifie 3 000, et en 10 minutes, 180 000 goroutines fuites consomment toute la mémoire. Pire, ces goroutines détiennent des références à des canaux qui bloquent le GC, finissant par faire planter tout le nœud.
Ce n'est pas un cas isolé. Les primitives de concurrence de Go sont simples — go func() lance la concurrence en une ligne — mais la programmation concurrente en production est bien plus que simplement démarrer des goroutines. Vous devez contrôler les niveaux de concurrence, gérer la propagation d'erreurs, implémenter un arrêt propre, et prévenir les fuites de ressources. Cet article couvre 7 motifs de concurrence de niveau production pour vous aider à construire des services Go concurrents robustes.
Référence des concepts clés
| Primitive | Objectif | Caractéristiques clés | Cas d'usage typique |
|---|---|---|---|
goroutine |
Unité d'exécution concurrente légère | Pile extensible (2KB initial), ordonnancement par le runtime Go | Toute tâche nécissant une exécution concurrente |
channel |
Communication inter-goroutines | Sûr pour les types, tamponnée/non tamponnée, fermable | Passage de données, notification de signal, collecte de résultats |
sync.WaitGroup |
Attendre un groupe de goroutines | Trio Add/Done/Wait |
Attente de tâches par lot, fan-out concurent |
sync.Mutex |
Exclusion mutuelle pour l'état partagé | Utilisable à valeur zéro, support TryLock (Go 1.18+) |
Compteurs, mises à jour de cache, rechargement à chaud de config |
context.Context |
Propager l'annulation et le timeout | Immuable, ne peut dériver que des contextes enfant | Timeout de requête, arrêt propre, traçage |
errgroup.Group |
Exécution concurrente + collecte d'erreurs | La première erreur annule toutes les goroutines | Appels API par lot, récupération de données parallèle |
semaphore.Weighted |
Limitation de débit par sémaphore pondéré | Supporte les poids, acquisition consciente du timeout | Limitation de débit API, contrôle de quota de ressources |
5 défis de la programmation concurrente en production
Défi 1 : la concurrence non bornée provoque l'épuisement des ressources
func handleRequests(urls []string) {
for _, url := range urls {
go fetch(url)
}
}
Chaque URL génère une goroutine — 10 000 URLs signifient 10 000 connexions concurrentes. Les pools de connexions de base de données s'épuisent, les services aval sont écrasés, la mémoire s'envole.
Défi 2 : fuites de goroutines
func process(ch <-chan int) {
for {
val := <-ch
fmt.Println(val)
}
}
Si le canal n'est jamais fermé, cette goroutine ne s'arrête jamais. Dans les services de longue durée, les goroutines fuites s'accumulent continuellement.
Défi 3 : erreurs silencieusement avalées
func fetchAll(urls []string) {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
log.Printf("fetch %s failed: %v", u, err)
return
}
process(resp)
}(url)
}
wg.Wait()
}
Les erreurs ne sont que journalisées — l'appelant n'a aucune idée des échecs. Si 2 URLs sur 3 échouent, l'appelant suppose que tout a réussi.
Défi 4 : pas d'arrêt propre (graceful shutdown)
Lorsque le service reçoit SIGTERM, les goroutines en cours sont interrompues de force. Les écritures en vol peuvent corrompre des données, les transactions en cours peuvent être à moitié terminées.
Défi 5 : difficulté de composer les motifs de concurrence
Le Worker Pool nécissite une limitation de débit, le Pipeline nécissite une propagation d'erreurs, le Fan-out nécissite une agrégation de résultats. Implémenter chaque motif seul n'est pas dur, mais les combiner dans un seul service crée des interactions qui mènent facilement aux interblocages (deadlocks).
7 motifs de concurrence de niveau production
Motif 1 : Worker Pool — pool de goroutines borné
Le Worker Pool est le motif de concurrence le plus fondamental et le plus important. Idée centrale : un nombre fixe d'ouvriers puisent des tâches dans une file, évitant la concurrence non bornée.
package workerpool
import (
"context"
"sync"
)
type Task func(ctx context.Context) error
type Pool struct {
workers int
tasks chan Task
wg sync.WaitGroup
}
func NewPool(workers int, bufferSize int) *Pool {
return &Pool{
workers: workers,
tasks: make(chan Task, bufferSize),
}
}
func (p *Pool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go func(workerID int) {
defer p.wg.Done()
for {
select {
case <-ctx.Done():
return
case task, ok := <-p.tasks:
if !ok {
return
}
_ = task(ctx)
}
}
}(i)
}
}
func (p *Pool) Submit(task Task) bool {
select {
case p.tasks <- task:
return true
default:
return false
}
}
func (p *Pool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Exemple d'utilisation :
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pool := NewPool(10, 100)
pool.Start(ctx)
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
}
for _, url := range urls {
u := url
pool.Submit(func(ctx context.Context) error {
return fetchURL(ctx, u)
})
}
pool.Stop()
}
Points de conception clés :
- Le nombre fixe d'ouvriers empêche l'explosion de goroutines
- Le canal de tâches tamponné sert de file de tâches
- Support du contexte pour l'annulation, les ouvriers peuvent sortir proprement
Submitest non bloquant, renvoie false quand la file est pleine
Motif 2 : Fan-out/Fan-in — diffusion-récolte parallèle
Le Fan-out distribue une source de données à plusieurs goroutines pour un traitement parallèle, le Fan-in fusionne les résultats de plusieurs goroutines dans un seul canal.
package fan
import (
"context"
"sync"
)
func FanOut[T any](ctx context.Context, source <-chan T, workers int) []<-chan T {
channels := make([]<-chan T, workers)
for i := 0; i < workers; i++ {
ch := make(chan T)
channels[i] = ch
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case val, ok := <-source:
if !ok {
return
}
select {
case ch <- val:
case <-ctx.Done():
return
}
}
}
}()
}
return channels
}
func FanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan T) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case val, ok := <-c:
if !ok {
return
}
select {
case out <- val:
case <-ctx.Done():
return
}
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Exemple d'utilisation — traitement parallèle de commandes :
func processOrders(ctx context.Context, orders <-chan Order) <-chan Result {
workers := FanOut(ctx, orders, 5)
return FanIn(ctx, workers...)
}
Points de conception clés :
- Support des génériques (Go 1.18+), applicable à n'importe quel type
- Chaque goroutine fan-out consomme indépendamment depuis le canal source
- Le Fan-in utilise WaitGroup pour attendre la fermeture de tous les canaux d'entrée
- Toutes les goroutines peuvent sortir à l'annulation du contexte
Motif 3 : Pipeline — traitement par étapes
Le Pipeline décompose un traitement complexe en étapes, chacune s'exécutant comme une goroutine reliée par des canaux.
package pipeline
import (
"context"
)
type Stage[In any, Out any] func(ctx context.Context, in <-chan In) <-chan Out
func NewPipeline[In any, Out any](
ctx context.Context,
source <-chan In,
stages ...Stage[In, In],
) <-chan Out {
current := source
for _, stage := range stages {
current = stage(ctx, current)
}
return any(current).(<-chan Out)
}
func NewStage[In any, Out any](
process func(ctx context.Context, in In) (Out, error),
bufferSize int,
) Stage[In, Out] {
return func(ctx context.Context, in <-chan In) <-chan Out {
out := make(chan Out, bufferSize)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case val, ok := <-in:
if !ok {
return
}
result, err := process(ctx, val)
if err != nil {
continue
}
select {
case out <- result:
case <-ctx.Done():
return
}
}
}
}()
return out
}
}
Exemple d'utilisation — pipeline de traitement de données :
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
raw := make(chan RawData, 100)
validate := NewStage[RawData, ValidData](func(ctx context.Context, in RawData) (ValidData, error) {
if err := in.Validate(); err != nil {
return ValidData{}, err
}
return in.ToValid(), nil
}, 50)
enrich := NewStage[ValidData, EnrichedData](func(ctx context.Context, in ValidData) (EnrichedData, error) {
return fetchExtraInfo(ctx, in)
}, 50)
transform := NewStage[EnrichedData, FinalData](func(ctx context.Context, in EnrichedData) (FinalData, error) {
return in.Transform()
}, 50)
result := NewPipeline(ctx, raw, validate, enrich, transform)
go func() {
for r := range result {
saveToDB(r)
}
}()
}
Points de conception clés :
- Chaque étape est une goroutine indépendante, peut être mise à l'échelle indépendamment
- Les canaux fournissent une contre-pression (backpressure) entre les étapes
- Les erreurs sont gérées dans les étapes, n'interrompent pas tout le pipeline
- Toutes les étapes sortent proprement à l'annulation du contexte
Motif 4 : errgroup — gestion concurrente des erreurs
errgroup est la version consciente des erreurs de sync.WaitGroup : la première erreur annule toutes les goroutines.
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
type FetchResult struct {
URL string
Body string
Size int
}
func fetchMultiple(ctx context.Context, urls []string) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("create request %s: %w", url, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read %s: %w", url, err)
}
results[i] = FetchResult{
URL: url,
Body: string(body),
Size: len(body),
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
errgroup avec limite de concurrence :
func fetchWithLimit(ctx context.Context, urls []string, maxConcurrent int) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrent)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
results[i], _ = fetchOne(ctx, url)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Points de conception clés :
errgroup.WithContextpropage automatiquement l'annulationg.SetLimit(n)contrôle la concurrence max (Go 1.20+)- La première erreur annule toutes les goroutines en cours
- La capture de variable de clôture nécissite
i, url := i, url
Motif 5 : Sémaphore — limitation de débit
semaphore.Weighted fournit des sémaphores pondérés pour l'allocation de ressources avec différents poids.
package ratelimit
import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
)
type RateLimiter struct {
sem *semaphore.Weighted
}
func NewRateLimiter(maxWeight int64) *RateLimiter {
return &RateLimiter{
sem: semaphore.NewWeighted(maxWeight),
}
}
func (r *RateLimiter) Do(ctx context.Context, weight int64, fn func() error) error {
if err := r.sem.Acquire(ctx, weight); err != nil {
return fmt.Errorf("acquire semaphore: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Exemple d'utilisation — limitation de débit API étagée :
func main() {
limiter := NewRateLimiter(100)
err := limiter.Do(context.Background(), 10, func() error {
return callLightAPI()
})
err = limiter.Do(context.Background(), 50, func() error {
return callHeavyAPI()
})
}
Sémaphore avec timeout :
func (r *RateLimiter) DoWithTimeout(ctx context.Context, weight int64, timeout time.Duration, fn func() error) error {
acquireCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if err := r.sem.Acquire(acquireCtx, weight); err != nil {
return fmt.Errorf("semaphore acquire timeout: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Points de conception clés :
- Sémaphore pondéré, différentes opérations consomment différents quotas
Acquiresupporte l'annulation de contexte et le timeoutReleasedoit être associé àAcquire- Adapté à la limitation de débit API étagée, au contrôle de quota de ressources
Motif 6 : annulation de contexte et timeout
Le contexte est le « fil vital » de la programmation concurrente Go, utilisé pour propager les signaux d'annulation, les timeouts et les échéances.
package ctxutil
import (
"context"
"fmt"
"time"
)
type Result struct {
Data string
Error error
}
func FetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("fetch %s timed out after %v: %w", url, timeout, ctx.Err())
}
return "", fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
return string(body), nil
}
func BatchFetch(ctx context.Context, urls []string, timeout time.Duration) []Result {
results := make([]Result, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
data, err := FetchWithTimeout(ctx, u, timeout)
results[idx] = Result{Data: data, Error: err}
}(i, url)
}
wg.Wait()
return results
}
Motif d'arrêt propre :
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
server := &http.Server{Addr: ":8080"}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
_ = server.ListenAndServe()
}
Points de conception clés :
WithTimeout/WithDeadlinepour définir des timeoutsWithCancelpour un contrôle d'annulation manuelsignal.NotifyContextpour écouter les signaux système- L'annulation de contexte se propage à toutes les goroutines enfant
defer cancel()prévient les fuites de contexte
Motif 7 : service concurrent de production — combinaison de motifs
Combinez les motifs ci-dessus pour construire un service concurrent de niveau production : Worker Pool + Pipeline + errgroup + Context.
package concurrencyservice
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
type Service struct {
workers int
bufferSize int
timeout time.Duration
}
func NewService(workers, bufferSize int, timeout time.Duration) *Service {
return &Service{
workers: workers,
bufferSize: bufferSize,
timeout: timeout,
}
}
type Job struct {
ID string
Input any
}
type Output struct {
Job Job
Result any
Err error
}
func (s *Service) Process(ctx context.Context, jobs []Job) ([]Output, error) {
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(s.workers)
jobCh := make(chan Job, s.bufferSize)
resultCh := make(chan Output, s.bufferSize)
g.Go(func() error {
defer close(jobCh)
for _, job := range jobs {
select {
case <-ctx.Done():
return ctx.Err()
case jobCh <- job:
}
}
return nil
})
var processWg sync.WaitGroup
for i := 0; i < s.workers; i++ {
processWg.Add(1)
go func() {
defer processWg.Done()
for job := range jobCh {
output := Output{Job: job}
output.Result, output.Err = s.processOne(ctx, job)
select {
case resultCh <- output:
case <-ctx.Done():
return
}
}
}()
}
go func() {
processWg.Wait()
close(resultCh)
}()
var outputs []Output
for result := range resultCh {
outputs = append(outputs, result)
}
if err := g.Wait(); err != nil {
return outputs, fmt.Errorf("service process: %w", err)
}
return outputs, nil
}
func (s *Service) processOne(ctx context.Context, job Job) (any, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return fmt.Sprintf("processed-%s", job.ID), nil
}
Points de conception clés :
- errgroup contrôle la concurrence + la propagation d'erreurs
- Canaux pour la distribution des tâches et la collecte des résultats
- Contexte pour un timeout et une annulation unifiés
- WaitGroup assure que tous les ouvriers terminent avant de fermer le canal de résultats
- Conception en couches : ordonnancement (errgroup) + exécution (goroutines ouvrières) + collecte (range resultCh)
5 pièges courants et corrections
Piège 1 : capture de variable de clôture
❌ Incorrect :
for _, url := range urls {
go func() {
fetch(url)
}()
}
Toutes les goroutines partagent la même variable url, finissant par récupérer la dernière URL.
✅ Correct :
for _, url := range urls {
url := url
go func() {
fetch(url)
}()
}
Piège 2 : canal non fermé provoque une fuite de goroutine
❌ Incorrect :
func producer() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Les consommateurs parcourant le canal ne finissent jamais, fuite de goroutine.
✅ Correct :
func producer() <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Piège 3 : position erreonée de WaitGroup Add
❌ Incorrect :
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1)
defer wg.Done()
doWork()
}()
}
wg.Wait()
Les goroutines n'ont peut-être pas encore atteint wg.Add(1), et wg.Wait() de la goroutine principale passe immédiatement.
✅ Correct :
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
Piège 4 : fuite de time.After dans select
❌ Incorrect :
select {
case result := <-ch:
process(result)
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
Chaque select crée un nouveau canal time.After. Des appels fréquents fuient des timers.
✅ Correct :
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-ch:
process(result)
case <-ctx.Done():
return ctx.Err()
}
Piège 5 : copie de Mutex
❌ Incorrect :
type SafeMap struct {
mu sync.Mutex
data map[string]string
}
func copyMap(m SafeMap) SafeMap {
return m
}
La valeur zéro de Mutex est « déverrouillée ». Copier un Mutex verrouillé provoque un interblocage ou une condition de concurrence.
✅ Correct :
type SafeMap struct {
mu *sync.Mutex
data map[string]string
}
func NewSafeMap() *SafeMap {
return &SafeMap{
mu: &sync.Mutex{},
data: make(map[string]string),
}
}
Référence de dépannage des erreurs
| Symptôme | Cause possible | Investigation | Solution |
|---|---|---|---|
fatal error: all goroutines are asleep - deadlock! |
Toutes les goroutines bloquées, aucune goroutine active | Vérifier l'appariement lecture/écriture de canal, select default | Assurer que les canaux ont producteurs et consommateurs, ajouter l'annulation de contexte |
| Le nombre de goroutines ne cesse de croître | Fuite de goroutine, canaux non fermés | Monitoring runtime.NumGoroutine(), profil goroutine pprof |
Assurer que toutes les goroutines ont des chemins de sortie, defer close(ch) |
| La mémoire ne cesse de croître | Les goroutines détiennent de grandes références d'objets, accumulation de canaux | Profil heap pprof, vérifier la taille du tampon de canal | Limiter le tampon de canal, libérer les références promptement |
Erreurs context canceled fréquentes |
Contexte amont annulé, timeout trop court | Vérifier la chaîne de contexte, vérifier que le timeout est raisonnable | Ajuster le timeout, distinguer les erreurs métier des erreurs de timeout |
| Résultats concurrents manquants | La goroutine sort avant d'écrire dans le canal | Vérifier la logique de sortie de goroutine, confirmer la fermeture du canal | Utiliser WaitGroup pour assurer que toutes les goroutines terminent avant de fermer le canal |
race condition détectée |
Plusieurs goroutines lisent/écrivent des variables partagées | go test -race, vérifier les variables globales et les captures de clôture |
Utiliser Mutex/RWMutex pour la protection, ou basculer vers la communication par canal |
| Canal bloqué, aucune réponse | Déséquilibre producteur/consommateur | Vérifier la taille du tampon de canal, monitor les taux de production/consommation | Augmenter le tampon, utiliser select+default pour un envoi non bloquant |
| Échec de l'arrêt propre | Les goroutines ne répondent pas à l'annulation de contexte | Vérifier si les goroutines font select sur ctx.Done() | Assurer que toutes les goroutines de longue durée vérifient ctx.Done() |
| errgroup ne renvoie qu'une seule erreur | errgroup conçu pour l'annulation à première erreur | Vérifier si vous devez collecter toutes les erreurs | Utiliser un collecteur d'erreurs personnalisé ou plusieurs errgroups |
| Timeout d'acquisition de sémaphore | Limite de concurrence trop stricte, requêtes en file trop longtemps | Monitor le temps d'attente du sémaphore, ajuster les quotas de poids | Augmenter la capacité du sémaphore, optimiser l'allocation des poids |
Techniques d'optimisation avancées
Optimisation 1 : Worker Pool dynamique
Ajustez dynamiquement le nombre d'ouvriers selon la charge système :
package dynamicpool
import (
"context"
"runtime"
"sync"
"sync/atomic"
"time"
)
type DynamicPool struct {
minWorkers int64
maxWorkers int64
active atomic.Int64
tasks chan Task
wg sync.WaitGroup
adjustTick *time.Ticker
}
func NewDynamicPool(minW, maxW int) *DynamicPool {
return &DynamicPool{
minWorkers: int64(minW),
maxWorkers: int64(maxW),
tasks: make(chan Task, maxW*2),
adjustTick: time.NewTicker(5 * time.Second),
}
}
func (p *DynamicPool) Start(ctx context.Context) {
for i := 0; i < int(p.minWorkers); i++ {
p.addWorker(ctx)
}
go func() {
for {
select {
case <-ctx.Done():
p.adjustTick.Stop()
return
case <-p.adjustTick.C:
p.adjustWorkers(ctx)
}
}
}()
}
func (p *DynamicPool) addWorker(ctx context.Context) {
p.active.Add(1)
p.wg.Add(1)
go func() {
defer func() {
p.active.Add(-1)
p.wg.Done()
}()
for task := range p.tasks {
_ = task(ctx)
}
}()
}
func (p *DynamicPool) adjustWorkers(ctx context.Context) {
current := p.active.Load()
queueLen := len(p.tasks)
if queueLen > int(current) && current < p.maxWorkers {
p.addWorker(ctx)
} else if queueLen == 0 && current > p.minWorkers {
for i := 0; i < 5 && int(p.active.Load()) > int(p.minWorkers); i++ {
p.tasks <- nil
}
}
}
func (p *DynamicPool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Optimisation 2 : regroupement par lots (Batching)
Combinez plusieurs petites tâches en une seule opération par lot pour réduire les surcoûts syscall et IO :
package batcher
import (
"context"
"sync"
"time"
)
type Batcher[T any, R any] struct {
batchSize int
flushInterval time.Duration
handler func(ctx context.Context, batch []T) ([]R, error)
mu sync.Mutex
buffer []T
results []R
}
func NewBatcher[T any, R any](
batchSize int,
flushInterval time.Duration,
handler func(ctx context.Context, batch []T) ([]R, error),
) *Batcher[T, R] {
return &Batcher[T, R]{
batchSize: batchSize,
flushInterval: flushInterval,
handler: handler,
buffer: make([]T, 0, batchSize),
}
}
func (b *Batcher[T, R]) Add(ctx context.Context, item T) (R, error) {
b.mu.Lock()
b.buffer = append(b.buffer, item)
if len(b.buffer) >= b.batchSize {
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
results, err := b.handler(ctx, batch)
if err != nil {
var zero R
return zero, err
}
b.results = append(b.results, results...)
var zero R
if len(results) > 0 {
return results[0], nil
}
return zero, nil
}
b.mu.Unlock()
var zero R
return zero, nil
}
func (b *Batcher[T, R]) Flush(ctx context.Context) ([]R, error) {
b.mu.Lock()
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
if len(batch) == 0 {
return b.results, nil
}
results, err := b.handler(ctx, batch)
if err != nil {
return nil, err
}
b.results = append(b.results, results...)
return b.results, nil
}
Optimisation 3 : passage par canal sans copie (Zero-Copy)
Utilisez des pointeurs et sync.Pool pour réduire l'allocation mémoire lors du passage par canal :
package zerocopy
import (
"sync"
)
type Buffer struct {
Data []byte
}
var bufferPool = sync.Pool{
New: func() any {
return &Buffer{Data: make([]byte, 0, 4096)}
},
}
func GetBuffer() *Buffer {
return bufferPool.Get().(*Buffer)
}
func PutBuffer(buf *Buffer) {
buf.Data = buf.Data[:0]
bufferPool.Put(buf)
}
func ProcessPipeline(ctx context.Context, input <-chan []byte) <-chan *Buffer {
out := make(chan *Buffer, 64)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case data, ok := <-input:
if !ok {
return
}
buf := GetBuffer()
buf.Data = append(buf.Data[:0], data...)
select {
case out <- buf:
case <-ctx.Done():
PutBuffer(buf)
return
}
}
}
}()
return out
}
Comparaison des motifs de concurrence
| Caractéristique | goroutine+channel | sync.WaitGroup | errgroup | Worker Pool |
|---|---|---|---|---|
| Contrôle de concurrence | Pas de contrôle intégré | Pas de contrôle intégré | SetLimit |
Nombre d'ouvriers fixe |
| Gestion d'erreurs | Implémentation manuelle | Implémentation manuelle | Annulation auto à première erreur | Implémentation manuelle |
| Contre-pression | Tampon de canal | Aucune | Aucune | Canal de tâches |
| Propagation d'annulation | Contexte manuel | Contexte manuel | Contexte auto | Contexte manuel |
| Collecte de résultats | Réception de canal | Variables partagées | Retourner un slice | Réception de canal |
| Cas d'usage | Traitement de flux de données | Attente de tâches par lot | Appels API concurrents | Traitement de tâches à débit limité |
| Complexité | Moyenne | Faible | Faible | Moyenne |
| Nombre de goroutines | Non fixé | Non fixé | Limité | Fixé |
| Recommandation production | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Résumé
La programmation concurrente Go ne consiste pas à « juste faire tourner » — elle consiste à répondre à quatre questions : Combien de goroutines tournent ? Quand sortent-elles ? Que se passe-t-il en cas d'erreur ? Comment les ressources sont-elles récupérées ? Le Worker Pool répond « combien », le Context répond « quand sortir », errgroup répond « que faire en erreur », et defer close() répond « comment récupérer ». Maîtrisez ces 7 motifs, et vous aurez la méthodologie centrale de la programmation concurrente Go de niveau production.
Outils recommandés
- JSON Formatter — Formater les réponses JSON des services concurrents, déboguer rapidement les problèmes de structure de données
- Hash Calculator — Calculer les signatures de requêtes et les checksums de données, assurer la cohérence des données entre requêtes concurrentes
- Base64 Encode/Decode — Gérer l'encodage des données binaires dans les services concurrents
Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →