Échange de données entre deux programmes Go avec les Gobs

Cet article est une traduction de l'article Gobs of data par Rob Pike (en anglais sur le site du projet Go).

Introduction

Pour transmettre une structure de données sur un réseau ou pour la stocker dans un fichier, elle doit être codée puis décodée de nouveau. Il existe de nombreuses méthodes d'codage, bien sûr: JSON, XML, le protocole de buffer protobuf de Google, etc. Et maintenant, il y en a un autre, fourni par Go : le paquet gob.

Pourquoi définir une nouvelle méthode de codage ? C'est à la fois beaucoup de travail et redondant. Pourquoi ne pas utiliser l'un des formats existants? Notez tout d'abord que Go a des paquets qui supportent tous les codages que nous venons de mentionner (le paquet gérant le protocole de buffer protobuf est dans un dépôt séparé, mais c'est l'un des paquets les plus souvent téléchargés). Et pour de nombreuses applications, y compris la communication avec les outils et les systèmes écrits dans d'autres langages, ces méthodes sont le bon choix.

Mais pour un environnement où Go est majoritaire, par exemple dans lequel deux serveurs écrits en Go doivent communiquer, il y a une possibilité de développer quelque chose de beaucoup plus facile à utiliser et peut-être de plus efficace.

Les Gobs fonctionnent avec le langage Go d'une manière qu'on ne peut pas reproduire avec les méthodes de codage agnostiques et conçues indépendamment du langage. En même temps, il y a des leçons à tirer des systèmes existants.

Objectifs

Le paquet gob fut conçu avec un certain nombre d'objectifs en tête.

Tout d'abord, et évidemment, il doit être très facile à utiliser. Premièrement, comme Go possède un mécanisme de réflexion, il n'est pas nécessaire d'utiliser un langage de définition d'interface séparé ou un « compilateur de protocole ». La structure de données elle-même est tout ce dont le paquet devrait avoir besoin pour comprendre comment la coder et la décoder. D'un autre côté, cette approche signifie que les Gobs ne fonctionneront jamais avec d'autres langages de programmation : les Gobs sont donc très spécifiques à Go.

L'efficacité est également importante. Les représentations textuelles, telles que XML et JSON, sont trop lentes pour être au centre d'un réseau de communication efficace. Un codage binaire est nécessaire.

Les flux Gob doivent être autodescriptifs. Chaque flux Gob, lu depuis le début, contient suffisamment de renseignements pour que la totalité du flux puisse être analysée par un agent qui ne sait rien sur son contenu a priori. Cette caractéristique signifie que vous serez toujours en mesure de décoder un flux Gob stocké dans un fichier, même longtemps après que vous ayez oublié les données qu'il représente.

Il y avait aussi des choses à apprendre de nos expériences avec le protocole protobuf de Google.

Les mauvaises options du protocole protobuf

Le protocole protobuf a eu une influence majeure sur la conception des Gobs, mais il a trois caractéristiques qui ont été délibérément mises de côté. (Sans parler du fait que les tampons protobuf ne sont pas autodescriptifs : si vous ne connaissez pas la définition de données utilisée pour coder un tampon protobuf, vous pourriez ne pas être en mesure de l'analyser).

Tout d'abord, les tampons protobuf ne fonctionnent que sur le type de données que nous appelons une structure en Go. Vous ne pouvez pas coder un entier ou un tableau au plus haut niveau, mais seulement une structure avec des champs à l'intérieur. Cela semble une restriction inutile, du moins avec Go. Si tout ce que vous voulez envoyer est un tableau d'entiers, pourquoi devriez-vous le mettre d'abord dans une structure ?

Ensuite, une définition protobuf peut préciser que les champs T.x et T.y doivent être présents à chaque fois qu'une valeur de type T est codée ou décodée. Bien que le fait de rendre des champs obligatoires peut sembler une bonne idée, ils sont coûteux à mettre en œuvre, car le codec doit maintenir une structure de données séparée pendant le codage et le décodage, pour être en mesure de rendre compte lorsque les champs requis sont manquants. Ils posent également un problème de maintenance. Au fil du temps, on peut vouloir modifier la définition de données pour supprimer un champ obligatoire, mais cela peut provoquer le plantage des clients existants et consommateurs de ces données. Il est préférable de ne pas les avoir du tout dans le codage. (le protocole protobuf permet également l'utilisation de champs optionnels. Mais si nous n'avons pas de champs obligatoires, tous les champs sont facultatifs point barre. Il y aura plus à dire sur les champs facultatifs un peu plus loin dans cet article.)

La troisième mauvaise option du protocole protobuf concerne les valeurs par défaut. Si un tampon protobuf omet la valeur d'un champ ayant une valeur par défaut, alors la structure décodée se comporte comme si le champ était défini sur cette valeur. Cette idée fonctionne très bien lorsque vous avez des méthodes getter et setter pour contrôler l'accès au champ, mais c'est plus difficile à gérer proprement lorsque le conteneur est une simple structure Go. Les champs obligatoires sont également difficiles à mettre en œuvre: où peut-on définir les valeurs par défaut, quels types ont-ils (est-ce du texte UTF-8 ? Une suite arbitraire d'octets ? Combien de bits pour un nombre à virgule flottante ?) Et malgré la simplicité apparente, il y avait un certain nombre de complications dans la conception et l'implémentation du protocole protobuf. Nous avons décidé de laisser tomber ces options pour les Gobs et de revenir à la règle par défaut de Go, triviale, mais efficace : sauf si vous définissez quelque chose de contraire, il existe une « valeur zéro » pour tel ou tel type – et il n'y a pas besoin de la transmettre.

Donc, les Gobs finissent par ressembler à une sorte de protocole protobuf généralisé et simplifié. Mais comment fonctionnent-ils?

Valeurs

Les données codées dans les Gobs n'ont pas de types tels que int8 ou uint16. Au lieu de cela, il s'agit d'informations un peu analogues aux constantes Go, les valeurs entières sont abstraites, des nombres sans taille, qui peuvent être signés ou pas. Lorsque vous codez une donnée de type int8, sa valeur est transmise comme un entier à longueur variable sans taille fixe. Lorsque vous codez un int64 , sa valeur est également transmise comme un entier à longueur variable sans taille fixe (les nombres signés et non signés sont traités distinctement, mais la même absence de taille s'applique également aux valeurs non signées). Si les deux ont la valeur « 7 », les bits envoyés sur le fil seront identiques. Lorsque le récepteur décode cette valeur, il la met dans la variable du récepteur, qui peut être de n'importe quel type représentant un entier. Ainsi un codeur peut envoyer un « 7 » qui vient d'une variable de type int8, mais le récepteur peut stocker cette valeur dans une variable de type int64. C'est très bien ainsi : la valeur est un nombre entier et tant qu'elle convient, tout fonctionne (si elle ne convient pas, une erreur se produit). Ce découplage de la taille de la variable donne une certaine souplesse pour le codage : nous pouvons agrandir le type de la variable contenant un entier durant l'évolution du logiciel, tout en étant encore capables de décoder des données plus anciennes.

Cette flexibilité s'applique également aux pointeurs. Avant la transmission, tous les pointeurs sont mis à plat. Les valeurs de type int8, *int8, **int8, ****int8, etc. sont toutes transmises en tant que valeurs entières, valeurs qui peuvent être stockées dans une variable de type int de n'importe quelle taille, ou de type *int, ou ******int, etc. Encore une fois, cela permet d'être souple.

Cette souplesse se produit aussi parce que, lors du décodage d'une structure, seuls les champs qui sont envoyés par le codeur sont stockés dans la destination. Prenons par exemple la valeur :

type T struct{ X, Y, Z int } // Seuls les champs exportés (1ère lettre du nom en majuscule) sont codés et décodés.
var t = T{X: 7, Y: 0, Z: 8}

le codage de t envoie uniquement les données « 7 » et « 8 ». Parce qu'elle est égale à zéro, la valeur de Y n'est pas envoyée, car il n'y a pas besoin d'envoyer une valeur égale à zéro.

Le récepteur peut décoder les données dans cette structure alternative :

type U struct{ X, Y *int8 } // Note: pointeurs vers int8
var u U

et acquérir une valeur de u avec seulement X initialisé (à l'adresse d'une variable de type int8 initialisée à 7) ; Le champ Z est ignoré - où voudriez-vous le mettre ? Lors du décodage d'une structure, on cherche les champs par correspondance de nom et par compatibilité de type, et seuls les champs qui existent dans les deux cas sont affectés. Cette approche simple est une réponse appropriée au problème du « champ optionnel » : le type T peut évoluer par l'ajout de champs, mais les récepteurs périmés fonctionneront toujours avec la partie du type qu'ils reconnaissent. Ainsi les Gobs fournissent la partie la plus intéressante de l'option des champs facultatifs – l'extensibilité – sans aucun mécanisme ou notation supplémentaire.

À partir d'entiers, nous pouvons construire tous les autres types: octets, chaînes de caractères, tableaux, des slice, des map et même des nombres à virgule flottante. Les valeurs à virgule flottante sont représentées selon le modèle binaire de la norme IEEE 754, stockées comme un entier, ce qui fonctionne très bien tant que vous connaissez leur type, ce qui est toujours le cas. Par ailleurs, les octets de cet entier sont envoyés dans un ordre inversé parce que la plupart des valeurs des nombres à virgule flottante, tels que les petits entiers, ont beaucoup de zéros à la fin ; octets de poids faible que nous pouvons éviter de transmettre.

Une fonctionnalité intéressante que Go rend possible avec les Gobs, c'est qu'ils vous permettent de définir votre propre codage si votre type satisfait les interfaces GobEncoder et GobDecoder, d'une manière analogue au paquet JSON avec les interfaces Marshaler et Unmarshaler, mais aussi à l'interface Stringer du paquet fmt. Cette fonctionnalité permet de représenter des caractéristiques particulières, d'imposer des contraintes, ou de cacher des secrets lorsque vous transmettez des données. Voir la documentation pour plus de détails.

Les types sur le fil

La première fois que vous envoyez un type donné, le paquet gob inclut dans les flux de données une description de ce type. En fait, ce qui arrive c'est que le codeur est utilisé pour coder, dans le format de codage standard des gob, une structure interne qui décrit le type et lui donne un numéro unique (les types de base, ainsi que l'agencement de la structure de description du type, sont prédéfinis par le logiciel pour l'amorçage). Après que le type ait été décrit, il peut être référencé par son numéro de type.

Ainsi, lorsque nous envoyons notre premier type T, le codeur gob envoie une description de T et l'étiquette avec un numéro de type, disons « 127 ». Toutes les valeurs, y compris la première, sont alors précédées par ce nombre, donc un flux de valeurs T ressemble à ça :

("define type id" 127, definition of type T)(127, T value)(127, T value), ...

Ces numéros de type permettent de décrire des types récursifs et d'envoyer des valeurs de ces types. Ainsi les gobs peuvent coder des types tels que des arbres :

type Node struct {
    Value       int
    Left, Right *Node
}

(On laisse au lecteur découvrir comment la règle du zéro par défaut rend ceci possible, même si les gobs ne permettent pas de représenter des pointeurs)

Avec les informations de type, un flux gob est entièrement autodescriptif à l'exception de l'ensemble des types d'amorçage, qui est un point de départ bien défini.

Compiler une machine à coder/décoder

(NDT: Il existe une « machine » pour chaque type transmis dans un flux gob)

La première fois que vous codez une valeur d'un type donné, le paquet gob construit une machine spécifique à ce type de données. Il utilise la réflexion sur le type afin de construire cette machine, mais une fois que la machine est construite, il ne dépend plus de la réflexion. La machine utilise paquet unsafe et quelques astuces pour convertir à grande vitesse les données en octets codés. Il pourrait utiliser la réflexion et éviter d'utiliser le paquet unsafe, mais il serait beaucoup plus lent. (Une approche à grande vitesse similaire est adoptée par le projet supportant le protocole protobuf en Go, dont la conception a été influencée par l'implémentation du paquet gob). Les valeurs suivantes du même type utilisent la machine déjà compilée, de sorte qu'elles peuvent être codées immédiatement.

Le décodage est similaire, mais plus difficile. Lorsque vous décodez une valeur, le paquet gob détient un slice d'octets représentant une valeur d'un type défini par le codeur et qui est à décoder, plus une valeur Go dans lequel la décoder. Le paquet gob construit une machine pour cette paire : le type gob envoyé sur le fil croisé avec le type Go fourni pour le décodage. Notez que la machine de décodage n'utilise pas la réflexion, mais des méthodes unsafe afin d'obtenir la vitesse maximale.

Usage

Il y a beaucoup de choses sous le capot, mais le résultat est un système de codage de transmission de données facile à l'utilisation et efficace. Voici un exemple complet montrant différents types codés et décodés. Notez comment il est facile d'envoyer et de recevoir des valeurs, tout ce que vous devez faire c'est fournir des valeurs et les variables au paquet gob et il fera tout le travail.

 

Le paquet rpc s'appuie sur les gobs en transformant cette automatisation de codage/décodage en transport pour les appels de méthode à travers le réseau. C'est un sujet pour un autre article.

Détails

La documentation du paquet gob, en particulier le fichier doc.go, développe de nombreux détails décrits ici et comprend un exemple concret et complet montrant comment l'encodage représente des données. Si vous êtes intéressé par les entrailles de l'implémentation des gobs, c'est un bon endroit pour commencer.


Étiquettes :   gob   golang 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Comment gérer efficacement les erreurs en golang ?

Préconisations officielles pour la gestion des erreurs dans un programme golang. Cet article complète les explications sur panic, defer et recover   Lire »

Comment écrire du code Go ?

Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go.   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Canaux et go routines avec ou sans état

Exemples d'utilisation du type chan et des go routines stateful et stateless   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)