Dans l'évolution de Go, peu de packages ont été aussi pivotaux ou mal compris que context. Depuis son introduction dans Go 1.7, il est devenu le mécanisme standard pour transporter des délais d'expiration, des signaux d'annulation et des valeurs spécifiques à la requête à travers les limites de l'API. Pour les développeurs intermédiaires à avancés, aller au-delà de l'utilisation de base est essentiel pour écrire des applications concurrentes robustes, maintenables et évolutives.
La philosophie fondamentale : Propagation, pas stockage
L'erreur principale que commettent les développeurs est de traiter context.Context comme un magasin de configuration global ou une base de données pour transmettre des données arbitraires. C'est incorrect. L'objectif officiel d'un contexte est de transporter des signaux d'annulation et des valeurs spécifiques à la requête à travers les limites de l'API, entre les processus. C'est un conteneur passif qui doit être passé par valeur, et non par pointeur, garantissant ainsi l'immutabilité.
Considérez le contexte comme une vue en lecture seule du cycle de vie de la requête actuelle. Vous ne devez jamais stocker de données qui ne sont pas explicitement requises par tous les appels en aval. Si un goroutine spécifique a besoin de données qui ne sont pas pertinentes pour la chaîne de requête plus large, utilisez plutôt des variables locales standard.
Création et annulation des contextes
Toute opération de contexte commence par la création. La méthode la plus courante est context.Background(), qui retourne un contexte vide et non annulable qui sert de racine à tous les autres contextes. À partir de cette racine, nous pouvons dériver des contextes enfants avec des comportements spécifiques.
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Créer un contexte racine
ctx := context.Background()
// Dériver un contexte avec un délai d'expiration
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel() // Toujours annuler pour éviter les fuites de ressources
// Simuler un travail qui pourrait prendre plus de temps que le délai d'expiration
select {
case <-time.After(5 * time.Second):
fmt.Println("Travail terminé")
case <-ctx.Done():
fmt.Println("Travail annulé ou expiré :", ctx.Err())
}
}
Le modèle defer cancel() est critique. Ne pas appeler la fonction d'annulation peut entraîner des fuites de mémoire, car les goroutines en attente du contexte ne se réveilleront jamais. Bien que Go 1.9+ optimise cela pour la plupart des contextes standards, cela reste une bonne pratique.
Gestion des délais d'expiration et des timeouts
Il existe deux types principaux de contextes temporisés : WithTimeout et WithDeadline. WithTimeout calcule le délai d'expiration par rapport à l'heure actuelle, tandis que WithDeadline accepte un time.Time spécifique. Dans les microservices à haut débit, l'utilisation de délais d'expiration empêche les défaillances en cascade. Si un service en aval se bloque, le timeout garantit que l'appelant ne bloque pas indéfiniment, libérant ainsi des ressources pour gérer d'autres requêtes.
Il est important de noter que les contextes sont hiérarchiques. Si un contexte parent est annulé, tous ses enfants sont automatiquement annulés. Cette fonctionnalité est incroyablement puissante pour implémenter une annulation en cascade dans des graphes de concurrence complexes.
Stockage de valeurs : À utiliser avec prudence
La méthode context.WithValue permet de stocker des paires clé-valeur. Cependant, cela doit être utilisé avec parcimonie et uniquement pour des valeurs spécifiques à la requête telles que les jetons d'authentification, les identifiants d'utilisateur ou les identifiants de traçage pour le traçage distribué. Ne l'utilisez jamais pour des paramètres optionnels ou des paramètres de configuration.
type ctxKey string
const userIDKey ctxKey = "userID"
func GetUserFromContext(ctx context.Context) string {
val, ok := ctx.Value(userIDKey).(string)
if !ok {
return "inconnu"
}
return val
}
func processRequest(ctx context.Context) {
// Utilisation sûre : stockage de métadonnées spécifiques à la requête
ctx = context.WithValue(ctx, userIDKey, "user-123")
// Transmission aux fonctions en aval
userID := GetUserFromContext(ctx)
fmt.Printf("Traitement pour l'utilisateur : %s\n", userID)
}
Évitez d'utiliser des littéraux int ou string comme clés, car ils peuvent entrer en collision avec d'autres packages utilisant le même modèle. Définissez un type personnalisé non exporté (comme ctxKey) pour garantir l'unicité des clés.
Meilleures pratiques pour la production
- Passer par valeur : Passez toujours
context.Contexten tant que premier argument des fonctions. - Ne stockez pas de secrets : Les contextes sont souvent journalisés ou sérialisés. Ne stockez pas de mots de passe ou de données personnelles sensibles (PII).
- Évitez les contextes dans les tâches de courte durée : Si une tâche n'a pas besoin d'annulation ou de délai d'expiration, n'utilisez pas de contexte. Cela ajoute une surcharge inutile.
- Vérifiez les erreurs : Vérifiez toujours
ctx.Err()pour distinguer l'annulation du timeout.
Conclusion
Comprendre context est une étape initiatique pour les développeurs Go sérieux. Il transforme vos applications de simples scripts en systèmes résilients capables de gérer une concurrence complexe, des arrêts gracieux et une gestion efficace des ressources. En respectant son cas d'utilisation prévu — la propagation plutôt que le stockage — vous pouvez écrire du code Go plus propre, plus sûr et plus efficace.