Le package context en Go en 2026 : Annulation, Timeouts et Questions d'Entretien

Guide complet sur le package context de Go : gestion de l'annulation, des timeouts et des deadlines. Inclut les questions fréquentes en entretien technique et les bonnes pratiques de production.

Le package context en Go - Annulation et Timeouts

Le package context constitue le mécanisme standard en Go pour propager les délais, les signaux d'annulation et les valeurs liées aux requêtes à travers les frontières d'API. Chaque application Go en production utilise le context : les handlers HTTP, les requêtes base de données, les appels gRPC et les workers en arrière-plan dépendent tous de ce package pour l'arrêt gracieux et la gestion des timeouts.

Essentiel en Entretien

Le context représente l'un des sujets les plus fréquemment abordés lors des entretiens Go. Les recruteurs attendent des candidats qu'ils expliquent la propagation du context, démontrent une gestion correcte de l'annulation et évitent les pièges courants comme le stockage du context dans des structures.

L'Interface Context et ses Quatre Méthodes

L'interface Context définit quatre méthodes que chaque implémentation doit satisfaire. Comprendre ces méthodes constitue la base d'une utilisation efficace du context.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() retourne un channel en lecture seule. Lorsque ce channel se ferme, toute goroutine à son écoute reçoit le signal immédiatement. Le pattern <-ctx.Done() bloque jusqu'à ce que l'annulation survienne. Err() explique la raison : soit context.Canceled lors d'une annulation explicite, soit context.DeadlineExceeded lorsqu'un timeout ou une deadline est dépassé.

Création de Contexts avec Background et TODO

Deux fonctions créent des contexts racines : context.Background() et context.TODO(). Les deux retournent des contexts non-nil et vides qui ne sont jamais annulés.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() sert de parent pour les requêtes entrantes, les fonctions main et le code d'initialisation. TODO() agit comme un placeholder durant le refactoring lorsque la source correcte du context n'est pas encore déterminée. Les outils d'analyse statique peuvent signaler l'utilisation de TODO() pour un suivi ultérieur.

Annulation avec WithCancel et WithCancelCause

L'annulation manuelle permet aux goroutines parentes de signaler aux enfants que le travail doit s'arrêter. Ce pattern apparaît dans les pools de workers, les tâches en arrière-plan et les implémentations d'arrêt gracieux.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, introduit dans Go 1.20, fournit des informations d'erreur plus riches que le simple WithCancel. La fonction context.Cause() récupère l'erreur passée à la fonction cancel, facilitant le débogage dans les systèmes complexes.

Prêt à réussir tes entretiens Go ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Timeouts et Deadlines pour le Traitement des Requêtes

Les handlers HTTP, les appels base de données et les requêtes vers des API externes doivent toujours avoir des timeouts. Le package context offre WithTimeout et WithDeadline à cet effet. Les deux créent des contexts qui s'annulent automatiquement lorsque le temps expire.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

Le pattern defer cancel() garantit que les ressources sont libérées même lorsque l'opération se termine avant le timeout. Omettre cet appel provoque une fuite de ressources : la goroutine du timer du context reste active jusqu'à l'expiration du timeout.

Valeurs Liées aux Requêtes avec WithValue

WithValue attache des données liées à une requête à un context. Les cas d'utilisation courants incluent les identifiants de requête, les tokens d'authentification et les spans de traçage. La documentation officielle souligne l'utilisation de types personnalisés comme clés pour éviter les collisions.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Les valeurs de context forment une chaîne immuable : chaque appel à WithValue crée un nouveau context enveloppant le parent. Les recherches parcourent cette chaîne, rendant les valeurs profondément imbriquées plus lentes à accéder. Ne stockez que des données liées aux requêtes, pas la configuration de l'application ni des paramètres optionnels.

Utilitaires WithoutCancel et AfterFunc

Go 1.21 a ajouté WithoutCancel pour les opérations qui doivent se terminer indépendamment de l'annulation du parent, comme les tâches de nettoyage. AfterFunc planifie un callback lorsque l'annulation du context survient.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel préserve les valeurs du context parent mais ignore son signal d'annulation. Ce pattern convient à la journalisation, l'émission de métriques et les commits base de données qui doivent réussir indépendamment du résultat de la requête.

Questions d'Entretien et Réponses

La préparation à un entretien technique Go nécessite une compréhension des mécanismes internes du context et des bonnes pratiques. Ces questions apparaissent fréquemment dans les entretiens des entreprises utilisant Go pour leurs services backend. Pour plus de préparation aux entretiens Go, consultez le module Questions d'entretien sur le Context Package.

Pourquoi le context doit-il être le premier paramètre ?

L'équipe Go a établi cette convention pour rendre la propagation du context visible et cohérente. Placer le context en premier signale aux lecteurs que la fonction respecte l'annulation. La bibliothèque standard suit ce pattern : http.Request.Context(), database/sql.QueryContext() et grpc.UnaryInterceptor attendent tous le context comme premier argument.

Que se passe-t-il si le context est stocké dans une structure ?

Stocker le context brise le modèle de cycle de vie des requêtes. Une structure peut survivre à la requête pour laquelle elle a été créée, causant des opérations qui utilisent des signaux d'annulation obsolètes ou manquent de nouvelles deadlines. La documentation du context met explicitement en garde contre ce pattern.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

Comment gérer le context dans les goroutines ?

Les goroutines doivent vérifier ctx.Done() dans leur boucle principale ou instruction select. Ignorer le channel done crée des fuites de goroutines : la fonction parente retourne, mais la goroutine continue à consommer des ressources.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

Quand utiliser context.TODO() ?

Utilisez TODO() lors d'un refactoring incrémental en ajoutant le support du context à du code legacy. Cela marque les emplacements qui nécessitent une propagation correcte du context. Le code de production devrait éventuellement remplacer tous les appels TODO() par des contexts réels provenant des handlers de requêtes ou de l'initialisation de l'application. Pour en savoir plus sur les patterns de concurrence Go, consultez Go Concurrency: Goroutines and Channels.

Erreurs Courantes à Éviter

Une mauvaise utilisation du context conduit à des bugs subtils qui se manifestent sous charge ou durant les séquences d'arrêt. Reconnaître ces patterns prévient les incidents de production.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Points Clés pour Utiliser le Context Go en Production

  • Passer le context comme premier paramètre des fonctions, jamais le stocker dans des structures
  • Toujours utiliser defer cancel() après WithTimeout, WithDeadline ou WithCancel pour prévenir les fuites de ressources
  • Utiliser WithCancelCause et context.Cause() pour un meilleur débogage des erreurs dans les chaînes d'annulation complexes
  • Vérifier <-ctx.Done() dans les boucles de goroutines pour permettre l'arrêt gracieux et prévenir les fuites
  • Utiliser des types de clés personnalisés pour WithValue afin d'éviter les collisions entre packages
  • Appliquer WithoutCancel pour les opérations de nettoyage qui doivent se terminer indépendamment de l'annulation du parent
  • Préférer context.Background() pour l'initialisation de l'application et context.TODO() uniquement durant le refactoring
Défi du jour

Tu saurais repérer le bug en Go ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 29 août 2026

Tags

#go
#context
#concurrency
#interview

Partager

Articles similaires