Écrire des plugins dans Go

Go ne pouvait pas charger le code de manière dynamique avant Go 1.8. Je suis un grand partisan des systèmes à base de plug-in, qui nécessitent dans de nombreux cas le chargement dynamique des plug-ins. J'ai même envisagé à un moment d'écrire un paquet de plugin basé sur l'intégration C.

Je suis super heureux que les concepteurs de Go aient ajouté cette capacité à la langue. Dans ce tutoriel, vous apprendrez pourquoi les plugins sont si importants, quelles sont les plateformes actuellement prises en charge et comment créer, construire, charger et utiliser des plugins dans vos programmes..   

La raison d'être des plugins Go

Les plugins Go peuvent être utilisés à plusieurs fins. Ils vous permettent de décomposer votre système en un moteur générique facile à raisonner et à tester, et de nombreux plug-ins adhèrent à une interface stricte avec des responsabilités bien définies. Les plugins peuvent être développés indépendamment du programme principal qui les utilise. 

Le programme peut utiliser différentes combinaisons de plugins et même plusieurs versions du même plugin en même temps. Les frontières nettes entre le programme principal et les plugins promeuvent la meilleure pratique de couplage lâche et de séparation des préoccupations.

Le paquet "plugin"

Le nouveau paquet "plug-in" introduit dans Go 1.8 a une portée et une interface très étroites. Il fournit le Ouvrir() fonction pour charger une bibliothèque partagée, qui retourne un objet Plugin. L'objet Plugin a un Chercher() fonction qui retourne un symbole (interface vide ) peut être typée comme une fonction ou une variable exposée par le plugin. C'est tout.

Support de plate-forme

Le package de plug-in est pris en charge uniquement sous Linux pour le moment. Mais comme vous le verrez, il existe des moyens de jouer avec des plugins sur n'importe quel système d'exploitation.

Préparation d'un environnement basé sur le docker

Si vous développez sur une machine Linux, il vous suffit d'installer Go 1.8 et vous êtes prêt à partir. Toutefois, si vous utilisez Windows ou macOS, vous avez besoin d’un conteneur Linux VM ou Docker. Pour l'utiliser, vous devez d'abord installer Docker.

Une fois Docker installé, ouvrez une fenêtre de console et tapez: docker run -it -v ~ / go: / go golang: 1.8-wheezy bash

Cette commande mappe mon local $ GOPATH à ~ / go à /aller à l'intérieur du conteneur. Cela me permet de modifier le code à l'aide de mes outils favoris sur l'hôte et de le mettre à disposition dans le conteneur pour la construction et l'exécution dans l'environnement Linux..

Pour plus d'informations sur Docker, jetez un œil à ma série "Docker du sol à la hausse" ici sur Envato Tuts +:

  • Docker à partir du sol: comprendre les images
  • Docker à partir du sol: Images de construction
  • Docker à partir du sol: Travailler avec des conteneurs, 1ère partie
  • Docker à partir du sol: Travailler avec des conteneurs, 2e partie

Créer un plugin Go

Un plug-in Go ressemble à un package standard, et vous pouvez également l'utiliser comme un package standard. Il ne devient un plugin que lorsque vous le construisez en tant que plugin. Voici quelques plugins qui implémentent une Trier()fonction qui trie une tranche d'entiers. 

QuickSort Plugin

Le premier plugin implémente un algorithme naïf QuickSort. L'implémentation fonctionne sur des tranches avec des éléments uniques ou avec des doublons. La valeur de retour est un pointeur sur une tranche d'entiers. Ceci est utile pour les fonctions de tri qui trient leurs éléments en place car cela permet de retourner sans copier.. 

Dans ce cas, je crée en réalité plusieurs tranches intermédiaires, de sorte que l'effet est principalement gaspillé. Je sacrifie les performances pour la lisibilité car l'objectif est de démontrer des plugins et non d'implémenter un algorithme super efficace. La logique va comme suit:

  • S'il n'y a aucun élément ou un élément, retourne la tranche d'origine (déjà triée)..
  • Choisissez un élément aléatoire comme une cheville.
  • Ajoutez tous les éléments inférieurs à la cheville au au dessous de tranche.
  • Ajoutez tous les éléments supérieurs à la cheville au au dessus de tranche. 
  • Ajouter tous les éléments qui sont égaux à la cheville à la milieu tranche.

À ce stade, le milieu slice est triée parce que tous ses éléments sont égaux (s'il y avait des doublons de la cheville, il y aurait plusieurs éléments ici). Vient maintenant la partie récursive. Il trie les au dessous de et au dessus de tranches en appelant Trier() encore. Lorsque ces appels reviendront, toutes les tranches seront triées. Ensuite, il suffit de les ajouter pour obtenir une sorte complète de la tranche d’éléments originale..

package principal d'importation "math / rand" func Sort (items [] int) * [] int if len (items) < 2  return &items  peg := items[rand.Intn(len(items))] below := make([]int, 0, len(items)) above := make([]int, 0, len(items)) middle := make([]int, 0, len(items)) for _, item := range items  switch  case item < peg: below = append(below, item) case item == peg: middle = append(middle, item) case item > peg: au-dessus = ajouter (au-dessus, élément) au-dessous = * Trier (au-dessous) = = Trier (au-dessus) trié: = ajouter (ajouter (ajouter, au-dessous, au milieu…), au-dessus…) retourne et trie

BubbleSort Plugin

Le second plugin implémente l'algorithme BubbleSort d'une manière naïve. BubbleSort est souvent considéré comme lent, mais pour un petit nombre d’éléments et avec quelques optimisations mineures, il bat souvent des algorithmes plus sophistiqués comme QuickSort.. 

En réalité, il est courant d'utiliser un algorithme de tri hybride commençant par QuickSort. Lorsque la récursion est suffisamment petite, l'algorithme bascule vers BubbleSort. Le plugin de tri à bulles implémente une Trier() fonctionner avec la même signature que l'algorithme de tri rapide. La logique va comme suit:

  • S'il n'y a aucun élément ou un élément, retourne la tranche d'origine (déjà triée)..
  • Itérer sur tous les éléments.
  • A chaque itération, parcourez le reste des éléments.
  • Échangez l'élément actuel avec n'importe quel élément plus grand.
  • À la fin de chaque itération, l'élément en cours sera à sa place..
paquetage principal func Sort (items [] int) * [] int if len (items) < 2  return &items  tmp := 0 for i := 0; i < len(items); i++  for j := 0; j < len(items)-1; j++  if items[j] > éléments [j + 1] tmp = éléments [j] éléments [j] = éléments [j + 1] éléments [j + 1] = tmp retour et éléments 

Construire le plugin

Nous avons maintenant deux plugins à créer pour créer une bibliothèque partageable pouvant être chargée dynamiquement par notre programme principal. La commande à construire est: allez construire -buildmode = plugin

Comme nous avons plusieurs plugins, j'ai placé chacun dans un répertoire séparé sous un répertoire "plugins" partagé. Voici la structure du répertoire des plugins. Dans chaque sous-répertoire du plugin, il y a le fichier source "_plugin.go "et un petit script shell" build.sh "pour construire le plugin. Les fichiers .so finaux sont placés dans le répertoire parent" plugins ":

$ plugins plugins ├── bubble_sort │ bubble_sort_plugin.go │ build.sh ├── bubble_sort_plugin.so ├── quick_sort │ build.sh └── quick_sort_plugin.go quick_sort_plugin .alors

La raison pour laquelle les fichiers * .so sont placés dans le répertoire plugins est qu'ils peuvent être facilement découverts par le programme principal, comme vous le verrez plus tard. La commande de construction réelle dans chaque script "build.sh" spécifie que le fichier de sortie doit être placé dans le répertoire parent. Par exemple, pour le plugin Bubble Sort, il s’agit de:

allez construire -buildmode = plugin -o… /bubble_sort_plugin.so

Chargement du plugin

Le chargement du plug-in nécessite de savoir où localiser les plug-ins cibles (les bibliothèques partagées * .so). Cela peut être fait de différentes façons:

  • passer des arguments en ligne de commande
  • définir une variable d'environnement
  • en utilisant un répertoire connu
  • en utilisant un fichier de configuration

Une autre préoccupation est de savoir si le programme principal connaît les noms de plug-in ou s'il découvre dynamiquement tous les plug-ins d'un répertoire donné. Dans l'exemple suivant, le programme s'attend à ce qu'il y ait un sous-répertoire appelé "plugins" dans le répertoire de travail actuel et charge tous les plugins trouvés..

L'appel à la filepath.Glob ("plugins / *. so") function renvoie tous les fichiers avec l'extension ".so" dans le sous-répertoire plugins, et plugin.Open (nom du fichier) charge le plugin. Si quelque chose ne va pas, le programme panique.

package main import ("fmt" "plugin" "chemin / chemin") func main () all_plugins, err: = chemin du fichier.Glob ("plugins / *. so") si err! = nil panic (err) pour _, nom_fichier: = plage (tous_plugins) fmt.Println (nomfichier) p, err: = plugin.Open (nomfichier) si err! = nil panic (err) 

Utiliser le plugin dans un programme

Localiser et charger le plugin n'est que la moitié de la bataille. L’objet plugin fournit le Chercher() méthode qui donne un nom de symbole renvoie une interface. Vous devez taper assert cette interface dans un objet concret (par exemple une fonction comme Trier()). Il n'y a aucun moyen de découvrir quels symboles sont disponibles. Il vous suffit de connaître leurs noms et leur type pour pouvoir taper assert correctement. 

Lorsque le symbole est une fonction, vous pouvez l'invoquer comme toute autre fonction après une assertion de type réussie. L'exemple de programme suivant illustre tous ces concepts. Il charge dynamiquement tous les plugins disponibles sans savoir quels plugins sont là, sauf qu'ils se trouvent dans le sous-répertoire "plugins". Il s'ensuit en recherchant le symbole "Trier" dans chaque plugin et en l'affirmant dans une fonction portant la signature. func ([] int) * [] int. Ensuite, pour chaque plugin, il appelle la fonction de tri avec une tranche d’entiers et affiche le résultat..

package main import ("fmt" "plugin" "chemin / chemin du fichier") func main () nombres: = [] int 5, 2, 7, 6, 1, 3, 4, 8 // Les plugins (les Les fichiers * .so) doivent figurer dans un sous-répertoire 'plugins' all_plugins, err: = chemin du fichier.Glob ("plugins / *. so") si err! = nil panic (err) pour _, nom du fichier: = plage (all_plugins) p, err: = plugin.Open (nom du fichier) si err! = nil panique (err) symbole, err: = p.Lookup ("Trier") si err! = nil panic () sortFunc, ok = symbole. (func ([] int) * [] int) if! ok panique ("Le plugin n'a pas de 'fonction Sort ([] int) [] int'") trié: = sortFunc (nombres) fmt.Println (nom de fichier, triés) Sortie: plugins / bubble_sort_plugin.so & [1 2 3 4 5 6 7 8] plugins / quick_sort_plugin.so & [1 2 3 4 5 6 7 8] 

Conclusion

Le paquet "plugin" fournit une base idéale pour l'écriture de programmes Go sophistiqués pouvant charger dynamiquement des plugins si nécessaire. L'interface de programmation est très simple et nécessite une connaissance détaillée du programme using sur l'interface du plugin.. 

Il est possible de construire un framework de plugin plus avancé et plus convivial sur le paquet "plugin". Espérons que cela sera bientôt porté sur toutes les plateformes. Si vous déployez vos systèmes sous Linux, envisagez d’utiliser des plug-ins pour rendre vos programmes plus flexibles et plus extensibles..