introduction
Nous sommes ravis d'annoncer la sortie d'une révision majeure de l'API Go pour les tampons de protocole , le format d'échange de données indépendant de la langue de Google.
Prérequis pour la mise à jour de l'API
Les premières liaisons de tampon de protocole pour Go ont été introduites par Rob Pike en mars 2010. Go 1 ne sortira pas avant deux ans.
Au cours des dix années écoulées depuis sa première sortie, le package a grandi et s'est développé avec Go. Les demandes de ses utilisateurs ont également augmenté.
Beaucoup de gens veulent écrire des programmes utilisant la réflexion pour travailler avec des messages de tampon de protocole. Le package
reflect
vous permet d'afficher les types et les valeurs Go, mais omet les informations système du type de tampon de protocole. Par exemple, nous pourrions avoir besoin d'écrire une fonction qui examine le journal entier et efface tout champ annoté comme contenant des données sensibles. Les annotations ne font pas partie du système de type de Go.
Un autre besoin courant est d'utiliser des structures de données autres que celles générées par le compilateur de tampon de protocole, comme un type de message dynamique capable de représenter des messages de type inconnu au moment de la compilation.
Nous avons également remarqué qu'une source courante de problèmes était que l'interface
proto.Message
, qui identifie les valeurs des types de messages générés, parcourt la description du comportement de ces types. Lorsque les utilisateurs créent des types qui implémentent cette interface (souvent par inadvertance en incorporant un message dans une autre structure) et transmettent des valeurs de ces types à des fonctions qui attendent des valeurs de message générées, les programmes se bloquent ou se comportent de manière imprévisible.
Les trois problèmes ont la même racine et une même solution: l'interface
Message
doit définir complètement le comportement du message et les fonctions qui opèrent sur des valeurs Message
doivent accepter librement tout type qui implémente correctement l'interface.
Puisqu'il est impossible de changer la définition existante du type Message tout en maintenant la compatibilité API du paquet, nous avons décidé qu'il était temps de commencer à travailler sur une nouvelle révision majeure incompatible du module protobuf.
Aujourd'hui, nous sommes heureux de publier ce nouveau module. Nous espérons que vous l'apprécierez.
Réflexion
La réflexion est la caractéristique phare de la nouvelle implémentation. Tout comme le package
reflect
fournit une vue des types et des valeurs de Go, le package google.golang.org/protobuf/reflect/protoreflect fournit une vue des valeurs en fonction du système de type de tampon de protocole.
Une description complète du package
protoreflect
prendrait trop de temps pour cet article, mais voyons néanmoins comment nous pourrions écrire la fonction de nettoyage du journal que nous avons mentionnée plus tôt.
Nous devons d'abord écrire un fichier
.proto
définissant une extension comme google.protobuf.FieldOptions afin que nous puissions annoter les champs comme confidentiels ou non.
syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
bool non_sensitive = 50000;
}
Nous pouvons utiliser cette option pour marquer certains champs comme non sensibles.
message MyMessage {
string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}
Ensuite, nous devons écrire une fonction Go qui prend une valeur de message arbitraire et supprime tous les champs sensibles.
// Redact pb.
func Redact(pb proto.Message) {
// ...
}
Cette fonction accepte
proto.Message
- une interface implémentée par tous les types de messages générés. Ce type est un alias pour le type défini dans le package protoreflect
:
type ProtoMessage interface{
ProtoReflect() Message
}
Pour éviter de remplir l'espace de noms de message généré, l'interface contient une seule méthode de retour
protoreflect.Message
qui permet d'accéder au contenu du message.
(Pourquoi alias? Parce qu'il
protoreflect.Message
a une méthode correspondante qui renvoie l'original proto.Message
, et nous devons éviter le cycle d'importation entre les deux packages.)
La méthode
protoreflect.Message.Range
appelle une fonction pour chaque champ rempli dans le message.
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// ...
return true
})
La fonction range est appelée en
protoreflect.FieldDescriptor
décrivant le type de tampon de protocole du champ et protoréflect.Valeur contenant la valeur du champ.
La méthode
protoreflect.FieldDescriptor.Options
renvoie les options de champ sous forme de message google.protobuf.FieldOptions
.
opts := fd.Options().(*descriptorpb.FieldOptions)
(Pourquoi assertion de type? Parce que le package généré
descriptorpb
dépend de protoreflect
, le package protoreflect ne peut pas renvoyer un type d'option spécifique sans appeler le cycle d'importation.)
Ensuite, nous pouvons vérifier les options pour voir la valeur de la variable booléenne de notre extension:
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true // non-sensitive
}
Notez que nous regardons ici le descripteur de champ, pas la valeur du champ. Les informations qui nous intéressent concernent le système de type de tampon de protocole, pas Go.
C'est également un exemple de domaine dans lequel nous avons simplifié l'API du
proto
package. L'original a proto.GetExtension
renvoyé à la fois une valeur et une erreur. Le nouveau proto.GetExtension
ne renvoie que la valeur, renvoyant la valeur par défaut du champ si elle est manquante. Les erreurs de décodage d'extension sont signalées à Unmarshal
.
Une fois que nous avons identifié le champ à modifier, il est assez facile de l'effacer:
m.Clear(fd)
En réunissant tout ce qui précède, notre fonction d'édition ressemble à ceci:
// Redact pb.
func Redact(pb proto.Message) {
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
opts := fd.Options().(*descriptorpb.FieldOptions)
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true
}
m.Clear(fd)
return true
})
}
Une meilleure version pourrait descendre récursivement dans les champs de valeur de message. Nous espérons que cet exemple simple fournira une introduction à la réflexion dans un tampon de protocole et à son utilisation.
Versions
Nous appelons les tampons de protocole de la version d'origine Go APIv1 et la version plus récente APIv2. Étant donné qu'APIv2 n'est pas rétrocompatible avec APIv1, nous devons utiliser des chemins de module différents pour chacun.
(Cette version de l'API n'est pas la même que la version du protocole tampon Langue:
proto1
, proto2
et proto3
. APIv1 et APIv2 - cette mise en œuvre particulière dans le Go, ce qui aide à la fois la version proto2
et proto3
)
dans le module github.com/golang/protobuf - APIv1.
Dans google.golang.org/protobuf Module - APIv2. Nous avons profité de la nécessité de modifier le chemin d'importation pour passer à un autre qui n'est pas lié à un fournisseur d'hébergement spécifique. (Nous avons considéré
google.golang.org/protobuf/v2
pour préciser qu'il s'agit de la deuxième version majeure de l'API, mais qui a choisi un chemin plus court comme meilleur choix à long terme.)
Nous comprenons que tous les utilisateurs ne migreront pas vers la nouvelle version majeure du package à la même vitesse. Certains changeront rapidement; d'autres peuvent rester indéfiniment sur l'ancienne version. Même dans le même programme, certaines parties peuvent utiliser une API et d'autres peuvent en utiliser une autre. Par conséquent, il est important que nous continuions à prendre en charge les programmes qui utilisent APIv1.
github.com/golang/protobuf@v1.3.4
Est la version la plus récente d'APIv1 avant APIv2.github.com/golang/protobuf@v1.4.0
Est une version d'APIv1 implémentée basée sur APIv2. L'API est la même, mais l'implémentation de base est prise en charge par la nouvelle API. Cette version contient des fonctions de conversion entreproto.Message
APIv1 et APIv2 pour faciliter la transition entre elles.google.golang.org/protobuf@v1.20.0
— APIv2.github.com/golang/protobuf@v1.4.0
, , APIv2, APIv1, .
(Pourquoi avons-nous commencé avec une version
v1.20.0
? Pour plus de clarté. Nous ne nous attendons pas à ce qu'APIv1 atteigne un jour v1.20.0
, un seul numéro de version devrait donc suffire pour distinguer de manière unique APIv1 et APIv2.)
Nous avons l'intention de continuer à prendre en charge APIv1 sans fixer de date limite.
Cette disposition garantit que tout programme n'utilisera qu'une seule implémentation de tampon de protocole, quelle que soit la version de l'API qu'il utilise. Cela permet aux programmes d'implémenter la nouvelle API progressivement ou pas du tout, tout en conservant les avantages de la nouvelle implémentation. Le principe du choix de la version minimale signifie que les programmes peuvent rester dans l'ancienne implémentation jusqu'à ce que les responsables décident de la mettre à jour vers la nouvelle (directement ou en mettant à jour les dépendances).
Fonctionnalités supplémentaires à rechercher
Le package
google.golang.org/protobuf/encoding/protojson
convertit les messages de tampon de protocole vers et à partir de JSON à l'aide du mappage JSON canonique et corrige également un certain nombre de problèmes avec l'ancien package jsonpb
qui étaient difficiles à modifier sans causer de nouveaux problèmes aux utilisateurs existants.
Le package
google.golang.org/protobuf/types/dynamicpb
fournit une implémentation proto.Message
pour les messages dont le type de tampon de protocole est déterminé au moment de l'exécution.
Le package
google.golang.org/protobuf/testing/protocmp
fournit des fonctions pour comparer le tampon de protocole d'un message à un paquet github.com/google/cmp
.
Ce package prend en
google.golang.org/protobuf/compiler/protogen
charge l'écriture de plugins de compilateur de tampon de protocole.
Conclusion
Le module
google.golang.org/protobuf
est une refonte majeure de la prise en charge de Go pour le tampon de protocole, offrant une prise en charge de première classe pour la réflexion, la messagerie personnalisée et une API nettoyée. Nous avons l'intention de continuer à prendre en charge l'ancienne API en tant que wrapper pour la nouvelle, permettant aux utilisateurs d'implémenter progressivement la nouvelle API à leur propre rythme.
Notre objectif avec cette mise à jour est de renforcer les avantages de l'ancienne API et de remédier à ses faiblesses. Au fur et à mesure que nous avons terminé chaque composant de la nouvelle implémentation, nous avons commencé à l'utiliser dans la base de code Google. Ce déploiement progressif nous a donné confiance à la fois dans la convivialité de la nouvelle API et dans les meilleures performances et l'exactitude de la nouvelle implémentation. Nous sommes convaincus qu'il est prêt pour la production.
Nous sommes très enthousiastes à propos de cette version et espérons qu'elle servira bien l'écosystème Go pour la prochaine décennie ou plus!
En savoir plus sur le cours.