Retour au cours

backend / go

Le package context

Leçon 151 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le rôle de context.Context dans une chaîne d'appels concurrents
  • Utiliser WithTimeout et WithDeadline pour borner la durée d'une opération
  • Annuler manuellement une opération en cours avec WithCancel
  • Lire une annulation avec ctx.Done() et ctx.Err()
  • Savoir pourquoi WithValue doit rester exceptionnel

Dans quel contexte ?

Un utilisateur ferme l'onglet de son navigateur avant qu'une requête HTTP lente ait fini de traiter sa demande côté serveur. Sans mécanisme d'annulation, le serveur continuerait à consommer du CPU et de la mémoire pour un résultat que plus personne n'attend. context.Context est le mécanisme standard de Go pour propager ce signal d'abandon à travers toute une chaîne d'appels, potentiellement sur plusieurs goroutines.

D'abord, un principe simple : ctx circule toujours en premier paramètre

Par convention forte de tout l'écosystème Go, une fonction potentiellement longue ou annulable reçoit un context.Context comme tout premier paramètre, nommé ctx. Cette convention est tellement répandue que go vet et les linters la vérifient automatiquement.

context.Background() est le point de départ standard : un contexte racine, jamais annulé, à partir duquel on dérive des contextes enfants avec des propriétés supplémentaires.

Une fois ce point de départ posé, trois façons de créer un contexte dérivé existent

Chacune répond à un besoin différent de contrôle sur la durée de vie de l'opération.

FonctionDéclenchement de l'annulationCas d'usage typique
WithTimeout(ctx, duree)Après une durée relativeUn appel réseau qui ne doit pas dépasser 2 secondes
WithDeadline(ctx, instant)À un instant précisUne opération qui doit finir avant minuit
WithCancel(ctx)Manuellement, via la fonction cancel retournéeArrêter des goroutines suite à une action utilisateur

Prérequis

Il faut être à l'aise avec select et les channels (leçons précédentes) : ctx.Done() retourne un channel qui se ferme à l'annulation, exactement comme les patterns déjà vus.

Il reste une règle stricte à respecter systématiquement

Chaque fonction WithTimeout/WithDeadline/WithCancel retourne à la fois un contexte et une fonction cancel. Cette fonction cancel DOIT être appelée, généralement via defer annuler() juste après la création du contexte, même si le délai a déjà expiré naturellement.

Piège fréquent

Oublier d'appeler cancel() (même quand le timeout s'est déjà déclenché) provoque une fuite de ressources internes au runtime Go : le contexte et ses éventuels contextes enfants restent en mémoire plus longtemps que nécessaire. C'est un des rares cas de "fuite mémoire" possible en Go malgré le garbage collector, et le linter go vet la signale généralement (lostcancel).

Ensuite, comment une fonction réagit-elle concrètement à l'annulation ?

ctx.Done() retourne un channel qui se ferme au moment de l'annulation (timeout dépassé ou cancel() appelé). ctx.Err() explique ensuite la raison précise : context.DeadlineExceeded ou context.Canceled. Une fonction bien écrite surveille ce channel dans un select, aux côtés de son travail normal.

Bonne pratique

Réserve context.WithValue à la propagation de métadonnées transverses comme un ID de trace ou un utilisateur authentifié — jamais pour faire transiter des paramètres métier normaux d'une fonction. Une fonction ne devrait jamais avoir besoin de fouiller un contexte pour trouver un argument qu'elle pourrait simplement recevoir en paramètre explicite.

Maintenant que tu maîtrises toute la boîte à outils de la concurrence Go, il est temps de revenir à l'organisation du code lui-même à plus grande échelle : comment structurer un vrai projet avec des modules et des packages.

Commandes & code

Le package context

context.Context propage annulation, deadlines et valeurs à travers une chaîne d'appels concurrents.

go
package main

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

// Convention Go : ctx est toujours le PREMIER parametre, nomme "ctx"
func appelerAPI(ctx context.Context, id int) (string, error) {
	select {
	case <-time.After(300 * time.Millisecond): // simule un appel reseau lent
		return fmt.Sprintf("donnees-%d", id), nil
	case <-ctx.Done():
		return "", ctx.Err() // context.Canceled ou context.DeadlineExceeded
	}
}

func main() {
	// context.Background() : racine, jamais annule, point de depart standard
	ctx := context.Background()

	// WithTimeout : annule automatiquement apres une duree
	ctxTimeout, annuler := context.WithTimeout(ctx, 100*time.Millisecond)
	defer annuler() // toujours appeler annuler, meme si le timeout expire deja

	_, err := appelerAPI(ctxTimeout, 1)
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("l'appel a depasse le delai imparti")
	}

	// WithCancel : annulation manuelle, utile pour arreter des goroutines
	ctxCancel, annulerManuel := context.WithCancel(ctx)
	go func() {
		time.Sleep(50 * time.Millisecond)
		annulerManuel() // declenche ctx.Done() pour tous les consommateurs
	}()
	_, err = appelerAPI(ctxCancel, 2)
	fmt.Println("erreur attendue:", err)

	// WithDeadline : annule a un instant precis (plutot qu'une duree relative)
	deadline := time.Now().Add(200 * time.Millisecond)
	ctxDeadline, annuler2 := context.WithDeadline(ctx, deadline)
	defer annuler2()

	// WithValue : propager des valeurs de requete (ID de trace, utilisateur...)
	// A UTILISER AVEC PARCIMONIE : pas pour des parametres de fonction normaux
	type cleContexte string
	const cleRequestID cleContexte = "requestID"
	ctxAvecValeur := context.WithValue(ctx, cleRequestID, "req-12345")

	traiterRequete := func(ctx context.Context) {
		if id, ok := ctx.Value(cleRequestID).(string); ok {
			fmt.Println("traitement de la requete:", id)
		}
	}
	traiterRequete(ctxAvecValeur)

	_ = ctxDeadline
}

Résumé

  • ctx context.Context est toujours le premier paramètre d'une fonction qui peut être longue/annulable.
  • WithTimeout/WithDeadline/WithCancel retournent un context.Context fille et une fonction cancel à defer.
  • ctx.Done() se ferme à l'annulation ; ctx.Err() explique pourquoi (Canceled ou DeadlineExceeded).
  • WithValue sert à propager des métadonnées de requête, jamais des paramètres métier.

Exercices pratiques

1 disponible
1

Mission : un serveur qui continue de travailler pour des clients partis

Objectif : Propager correctement une annulation de contexte à travers un appel réseau, et diagnostiquer une fuite de ressources liée à un cancel oublié.

Contexte

Le serveur de recherche produit expose GET /recherche qui appelle appelerAPI(ctx, id), un appel réseau externe pouvant prendre jusqu'à 2 secondes. Le monitoring révèle que même après qu'un client ait fermé sa connexion (page fermée), l'appel réseau côté serveur continue de tourner jusqu'à son terme naturel, gaspillant CPU et connexions sortantes.

Résoudre l’exercice →