Escribiendo Plugins en Go

Go no pudo cargar el código dinámicamente antes de Go 1.8. Soy un gran defensor de los sistemas basados ​​en complementos, que en muchos casos requieren la carga dinámica de complementos. Incluso consideré en algún momento escribir un paquete de plugin basado en la integración C.

Estoy súper emocionado de que los diseñadores de Go hayan agregado esta capacidad al lenguaje. En este tutorial, aprenderá por qué los complementos son tan importantes, qué plataformas son compatibles actualmente y cómo crear, crear, cargar y usar complementos en sus programas..   

La justificación de los complementos de Go

Los complementos de Go se pueden utilizar para muchos propósitos. Le permiten dividir su sistema en un motor genérico que es fácil de razonar y probar, y muchos complementos se adhieren a una interfaz estricta con responsabilidades bien definidas. Los complementos pueden ser desarrollados independientemente del programa principal que los usa.. 

El programa puede usar diferentes combinaciones de complementos e incluso varias versiones del mismo complemento al mismo tiempo. Los límites nítidos entre el programa principal y los complementos promueven la mejor práctica de acoplamiento suelto y separación de preocupaciones.

El paquete "plugin"

El nuevo paquete de "complementos" introducido en Go 1.8 tiene un alcance y una interfaz muy estrechos. Proporciona la Abierto() función para cargar una biblioteca compartida, que devuelve un objeto Plugin. El objeto Plugin tiene una Buscar() La función que devuelve un símbolo (interfaz vacía ) puede ser confirmada por una función o variable expuesta por el complemento. Eso es.

Soporte de plataforma

El paquete de complementos solo es compatible con Linux en este momento. Pero hay formas, como verás, de jugar con complementos en cualquier sistema operativo.

Preparación de un entorno basado en Docker

Si está desarrollando en una caja de Linux, solo necesita instalar Go 1.8 y estará listo. Pero, si estás en Windows o macOS, necesitas una VM de Linux o un contenedor Docker. Para usarlo, primero debes instalar Docker..

Una vez que tengas instalado Docker, abre una ventana de consola y escribe: ventana acoplable ejecutar -v -v ~ / go: / go golang: 1.8-wheezy bash

Este comando mapea mi local $ GOPATH a ~ / ir a /ir dentro del contenedor. Eso me permite editar el código con mis herramientas favoritas en el host y tenerlo disponible dentro del contenedor para construir y ejecutar en el entorno Linux.

Para obtener más información sobre Docker, consulte mi serie "Docker From the Ground Up" aquí en Envato Tuts +:

  • Docker desde la base: comprensión de las imágenes
  • Docker From the Ground Up: imágenes de construcción
  • Docker desde el principio: trabajando con contenedores, parte 1
  • Docker desde el principio: trabajando con contenedores, parte 2

Creación de un complemento de Go

Un complemento de Go se parece a un paquete regular, y también puede usarlo como un paquete regular. Se convierte en un complemento solo cuando lo construyes como un complemento. Aquí hay un par de complementos que implementan un Ordenar()Función que ordena una porción de enteros.. 

Complemento QuickSort

El primer complemento implementa un ingenuo algoritmo QuickSort. La implementación funciona en segmentos con elementos únicos o con duplicados. El valor de retorno es un puntero a un segmento de enteros. Esto es útil para las funciones de clasificación que ordenan sus elementos en su lugar porque permite regresar sin copiar. 

En este caso, en realidad creo múltiples cortes interinos, por lo que el efecto se desperdicia principalmente. Aquí sacrifico el rendimiento por la legibilidad ya que el objetivo es demostrar los complementos y no implementar un algoritmo súper eficiente. La lógica es la siguiente:

  • Si hay cero elementos o un elemento, devuelva la división original (ya ordenada).
  • Elige un elemento aleatorio como una clavija.
  • Agrega todos los elementos que son menores que la clavija al abajo rebanada.
  • Agrega todos los elementos que son mayores que la clavija a la encima rebanada. 
  • Agrega todos los elementos que son iguales a la clavija a la medio rebanada.

En este punto, el medio la división se ordena porque todos sus elementos son iguales (si hubiera duplicados de la trazabilidad, habrá varios elementos aquí). Ahora viene la parte recursiva. Ordena el abajo y encima rebanadas llamando Ordenar() otra vez. Cuando esas llamadas regresen, todas las porciones serán ordenadas. Luego, simplemente agregándoles los resultados en una clase completa de la porción original de artículos.

paquete importación principal "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: arriba = agregar (arriba, artículo) abajo = * Ordenar (abajo) arriba = * Ordenar (arriba) ordenado: = agregar (adjuntar (abajo, medio ...), arriba ...) retorno y ordenado

BubbleSort Plugin

El segundo complemento implementa el algoritmo BubbleSort de una manera ingenua. BubbleSort a menudo se considera lento, pero para una pequeña cantidad de elementos y con una pequeña optimización, a menudo supera algoritmos más sofisticados como QuickSort. 

En realidad, es común utilizar un algoritmo de clasificación híbrida que comience con QuickSort, y cuando la recursión es lo suficientemente pequeña, el algoritmo cambia a BubbleSort. El plugin de tipo burbuja implementa un Ordenar() Funciona con la misma firma que el algoritmo de ordenación rápida. La lógica es la siguiente:

  • Si hay cero elementos o un elemento, devuelva la división original (ya ordenada).
  • Iterar sobre todos los elementos..
  • En cada iteración, itera sobre el resto de los elementos.
  • Intercambia el objeto actual con cualquier elemento que sea mayor.
  • Al final de cada iteración, el elemento actual estará en su lugar correcto.
paquete main 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] > items [j + 1] tmp = items [j] items [j] = items [j + 1] items [j + 1] = tmp return & items 

Construyendo el Plugin

Ahora, tenemos dos complementos que debemos crear para crear una biblioteca que se pueda compartir y que nuestro programa principal pueda cargar dinámicamente. El comando para construir es: ve a construir -buildmode = plugin

Ya que tenemos varios complementos, coloqué cada uno en un directorio separado debajo de un directorio compartido de "complementos". Aquí está el diseño del directorio del directorio de complementos. En cada subdirectorio de plugin, está el archivo fuente "_plugin.go "y un pequeño script de shell" build.sh "para compilar el complemento. Los archivos .so finales se guardan en el directorio principal de" complementos ":

$ tree 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 .asi que

La razón por la que los archivos * .so entran en el directorio de complementos es que el programa principal puede detectarlos fácilmente, como veremos más adelante. El comando de compilación real en cada secuencia de comandos "build.sh" especifica que el archivo de salida debe ir al directorio principal. Por ejemplo, para el plugin de tipo burbuja es:

vaya a construir -buildmode = complemento -o… /bubble_sort_plugin.so

Cargando el plugin

La carga del complemento requiere conocimiento de dónde ubicar los complementos de destino (las bibliotecas compartidas * .so). Esto se puede hacer de varias maneras:

  • pasando argumentos de linea de comando
  • establecer una variable de entorno
  • usando un directorio conocido
  • usando un archivo de configuración

Otra preocupación es si el programa principal conoce los nombres de los complementos o si descubre dinámicamente todos los complementos en un directorio determinado. En el siguiente ejemplo, el programa espera que haya un subdirectorio llamado "complementos" en el directorio de trabajo actual, y carga todos los complementos que encuentra.

La llamada a la filepath.Glob ("plugins / *. so") La función devuelve todos los archivos con la extensión ".so" en el subdirectorio de complementos, y plugin.Open (nombre de archivo) carga el plugin. Si algo sale mal, el programa entra en pánico..

package main import ("fmt" "plugin" "path / filepath") func main () all_plugins, err: = filepath.Glob ("plugins / *. so") si err! = nil panic (err) para _, filename: = rango (all_plugins) fmt.Println (filename) p, err: = plugin.Open (filename) si err! = nil panic (err) 

Usando el complemento en un programa

Localizar y cargar el complemento es solo la mitad de la batalla. El objeto plugin proporciona la Buscar() método que dado un nombre de símbolo devuelve una interfaz. Debe escribir asertar esa interfaz en un objeto concreto (por ejemplo, una función como Ordenar()). No hay manera de descubrir qué símbolos están disponibles. Solo tiene que saber sus nombres y su tipo, para poder escribir asertar correctamente. 

Cuando el símbolo es una función, puede invocarlo como cualquier otra función después de una aserción de tipo exitosa. El siguiente programa de ejemplo demuestra todos estos conceptos. Carga dinámicamente todos los complementos disponibles sin saber qué complementos están allí, excepto que están en el subdirectorio "complementos". Se sigue buscando el símbolo "Ordenar" en cada complemento y el tipo que lo afirma en una función con la firma func ([] int) * [] int. Luego, para cada complemento, invoca la función de clasificación con una porción de enteros e imprime el resultado.

package main import ("fmt" "plugin" "path / filepath") func main () numbers: = [] int 5, 2, 7, 6, 1, 3, 4, 8 // Los complementos (el Los archivos * .so) deben estar en un subdirectorio 'plugins' all_plugins, err: = filepath.Glob ("plugins / *. so") si err! = nil panic (err) para _, filename: = range (all_plugins) p, err: = plugin.Open (nombre de archivo) if err! = nil panic (err) símbolo, err: = p.Lookup ("Sort") if err! = nil panic (err) sortFunc, ok = símbolo. (func ([] int) * [] int) if! ok panic ("El complemento no tiene 'Sort ([] int) [] int' function") ordenado: = sortFunc (números) fmt.Println (nombre de archivo, ordenado) Salida: 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] 

Conclusión

El paquete de "complementos" proporciona una excelente base para escribir programas Go sofisticados que pueden cargar dinámicamente los complementos según sea necesario. La interfaz de programación es muy simple y requiere un conocimiento detallado del programa de uso en la interfaz del complemento. 

Es posible crear un marco de complemento más avanzado y fácil de usar sobre el paquete "complemento". Con suerte, será portado a todas las plataformas pronto. Si implementa sus sistemas en Linux, considere usar complementos para hacer que sus programas sean más flexibles y extensibles.