Avec le package context
de Go, il est possible de passer des données spécifiques aux requêtes à la pile des fonctions de gestion des requêtes en utilisant
func WithValue(parent Context, key, val interface{}) Context
Cela crée un nouveau Context
qui est une copie du parent et contient la valeur val accessible à l'aide de la clé.
Comment dois-je procéder si je souhaite stocker plusieurs paires clé-valeur dans un Context
? Dois-je appeler WithValue()
plusieurs fois, en passant à chaque fois le Context
reçu de mon dernier appel à WithValue()
? Cela semble lourd.
Ou dois-je utiliser une structure et y mettre toutes mes données, s.t. J'ai besoin de passer une seule valeur (qui est la structure), à partir de laquelle toutes les autres sont accessibles?
Ou existe-t-il un moyen de passer plusieurs paires clé-valeur à WithValue()
?
Vous avez à peu près énuméré vos options. La réponse que vous recherchez dépend de la façon dont vous souhaitez utiliser les valeurs stockées dans le contexte.
context.Context
est un objet immuable, "l'étendre" avec une paire clé-valeur n'est possible qu'en en faisant une copie et en ajoutant la nouvelle valeur-clé à la copie (qui est fait sous le capot, par le paquet context
).
Voulez-vous que d'autres gestionnaires puissent accéder à toutes les valeurs par clé de manière transparente? Ajoutez ensuite tout dans une boucle, en utilisant toujours le contexte de la dernière opération.
Une chose à noter ici est que le context.Context
N'utilise pas un map
sous le capot pour stocker les paires clé-valeur, ce qui peut sembler surprenant au début, mais pas si vous y pensez doit être immuable et sûr pour une utilisation simultanée.
Utilisation d'un map
Ainsi, par exemple, si vous avez beaucoup de paires clé-valeur et devez rechercher des valeurs par clés rapide, l'ajout de chacune séparément entraînera un Context
dont Value()
la méthode sera lente. Dans ce cas, il est préférable d'ajouter toutes vos paires clé-valeur en une seule valeur map
, accessible via Context.Value()
, et chaque valeur peut être interrogée par la clé associée dans O(1)
heure. Sachez cependant que cela ne sera pas sûr pour une utilisation simultanée, car une carte peut être modifiée à partir de goroutines simultanées.
Utilisation d'un struct
Si vous utilisez une grande valeur struct
contenant des champs pour toutes les paires clé-valeur que vous souhaitez ajouter, cela peut également être une option viable. Accéder à cette structure avec Context.Value()
vous renverrait une copie de la structure, donc ce serait sûr pour une utilisation simultanée (chaque goroutine ne pourrait obtenir qu'une copie différente), mais si vous avez plusieurs paires clé-valeur, cela entraînerait une copie inutile d'une grande structure chaque fois que quelqu'un en a besoin d'un seul champ.
Utilisation d'une solution hybride
Une solution hybride pourrait consister à placer toutes vos paires clé-valeur dans un map
et à créer une structure de wrapper pour cette carte, en masquant le map
(non exporté et fournir uniquement un getter pour les valeurs stockées dans la carte. En ajoutant uniquement ce wrapper au contexte, vous conservez le accès simultané sûr pour plusieurs goroutines (map
n'est pas exporté), pourtant aucun big data n'a besoin d'être copié (map
les valeurs sont de petits descripteurs sans les données de valeur-clé), et ce sera toujours rapide (comme vous finirez par indexer une carte).
Voici à quoi cela pourrait ressembler:
type Values struct {
m map[string]string
}
func (v Values) Get(key string) string {
return v.m[key]
}
En l'utilisant:
v := Values{map[string]string{
"1": "one",
"2": "two",
}}
c := context.Background()
c2 := context.WithValue(c, "myvalues", v)
fmt.Println(c2.Value("myvalues").(Values).Get("2"))
Sortie (essayez-le sur le Go Playground ):
two
Si les performances ne sont pas critiques (ou si vous avez relativement peu de paires clé-valeur), j'irais en ajoutant chacune séparément.
Oui, vous avez raison, vous devrez appeler WithValue()
en transmettant les résultats à chaque fois. Pour comprendre pourquoi cela fonctionne de cette façon, il vaut la peine de réfléchir un peu à la théorie derrière le contexte.
Un contexte est en fait un nœud dans une arborescence de contextes (d'où les différents constructeurs de contexte prenant un contexte "parent"). Lorsque vous demandez une valeur à partir d'un contexte, vous demandez en fait la première valeur trouvée qui correspond à votre clé lors de la recherche dans l'arborescence, à partir du contexte en question. Cela signifie que si votre arbre a plusieurs branches, ou si vous partez d'un point supérieur d'une branche, vous pouvez trouver une valeur différente. Cela fait partie du pouvoir des contextes. Les signaux d'annulation, d'autre part, se propagent le long de l'arbre à tous les éléments enfants de celui qui est annulé, de sorte que vous pouvez annuler une branche singal ou annuler l'arbre entier.
Pour un exemple, voici une arborescence de contexte qui contient diverses choses que vous pourriez stocker dans des contextes:
Les bords noirs représentent les recherches de données et les bords gris représentent les signaux d'annulation. Notez qu'ils se propagent dans des directions opposées.
Si vous deviez utiliser une carte ou une autre structure pour stocker vos clés, ce serait plutôt casser le point des contextes. Vous ne pourrez plus annuler uniquement une partie d'une demande, ou par exemple. changer où les choses ont été enregistrées selon la partie de la demande dans laquelle vous étiez, etc.
TL; DR - Oui, appelez WithValue plusieurs fois.
Comme l'a dit "icza", vous pouvez regrouper les valeurs dans une structure:
type vars struct {
lock sync.Mutex
db *sql.DB
}
Ensuite, vous pouvez ajouter cette structure en contexte:
ctx := context.WithValue(context.Background(), "values", vars{lock: mylock, db: mydb})
Et vous pouvez le récupérer:
ctxVars, ok := r.Context().Value("values").(vars)
if !ok {
log.Println(err)
return err
}
db := ctxVars.db
lock := ctxVars.lock