Plugins dinámicos en Go: plugin.Open, Lookup y el sistema de plugins nativo

El paquete plugin de Go permite cargar ficheros .so compilados en modo plugin en tiempo de ejecución, sin necesidad de recompilar la aplicación principal. Es Go nativo: los símbolos del plugin son funciones y variables Go reales que simplemente se cargan dinámicamente.

Compilar un plugin

// plugins/saludo/saludo.go
package main // los plugins deben ser package main

import "fmt"

// Saludar es el símbolo que exportaremos
func Saludar(nombre string) string {
    return fmt.Sprintf("Hola, %s! (desde el plugin)", nombre)
}

// Versión es una variable exportada
var Version = "1.0.0"

// El plugin no tiene función main ejecutable,
// pero sí puede tener init() para inicialización
func init() {
    fmt.Println("Plugin de saludo cargado")
}
go build -buildmode=plugin -o plugins/saludo.so plugins/saludo/saludo.go

Cargar el plugin con plugin.Open

package main

import (
    "fmt"
    "log"
    "plugin"
)

func main() {
    // Abrir el fichero .so
    p, err := plugin.Open("plugins/saludo.so")
    if err != nil {
        log.Fatalf("error abriendo plugin: %v", err)
    }

    // Buscar el símbolo por nombre (distingue mayúsculas)
    sym, err := p.Lookup("Saludar")
    if err != nil {
        log.Fatalf("símbolo no encontrado: %v", err)
    }

    // Cast al tipo función esperado
    saludar, ok := sym.(func(string) string)
    if !ok {
        log.Fatalf("tipo de símbolo incorrecto")
    }

    resultado := saludar("Ana")
    fmt.Println(resultado) // "Hola, Ana! (desde el plugin)"

    // Buscar una variable
    verSym, err := p.Lookup("Version")
    if err == nil {
        version := *verSym.(*string)
        fmt.Println("Versión del plugin:", version)
    }
}

Patrón con interfaz para plugins tipados

El patrón más robusto es definir una interfaz en el programa principal e implementarla en cada plugin. El cast al tipo interfaz es más seguro que el cast a funciones concretas:

// shared/plugin.go (código compartido entre host y plugins)
package shared

type Procesador interface {
    Procesar(datos []byte) ([]byte, error)
    Nombre() string
    Version() string
}

// El plugin implementa la interfaz
// plugins/compresor/compresor.go
package main

import (
    "compress/gzip"
    "bytes"
    "tuapp/shared"
)

type compresor struct{}

func (c *compresor) Procesar(datos []byte) ([]byte, error) {
    var buf bytes.Buffer
    w := gzip.NewWriter(&buf)
    _, err := w.Write(datos)
    w.Close()
    return buf.Bytes(), err
}

func (c *compresor) Nombre() string   { return "compresor-gzip" }
func (c *compresor) Version() string  { return "1.0.0" }

// Símbolo exportado que el host usa para obtener la implementación
var Impl shared.Procesador = &compresor{}

// En el host:
sym, _ := p.Lookup("Impl")
impl := *sym.(*shared.Procesador)
resultado, err := impl.Procesar(datos)

Restricciones de plataforma

El sistema de plugins de Go tiene limitaciones importantes que debes conocer antes de comprometerte con esta arquitectura:

// Los plugins solo funcionan en:
// - Linux (ELF)
// - macOS (Mach-O)
// NO funcionan en:
// - Windows
// - Plan 9
// - WebAssembly

// Verificar soporte en tiempo de compilación
//go:build linux || darwin

Además, el host y todos los plugins deben compilarse con exactamente la misma versión de Go y los mismos flags. Si hay incompatibilidad, plugin.Open devuelve un error. No es posible actualizar un plugin sin reiniciar el proceso host.

Alternativas: hashicorp/go-plugin

Para la mayoría de casos de uso reales, hashicorp/go-plugin es mejor que el paquete plugin nativo:

// hashicorp/go-plugin ejecuta cada plugin como un subproceso
// independiente y se comunica vía RPC o gRPC.
// Ventajas:
// - Funciona en Windows
// - Aislamiento de memoria: un plugin que crashea no tumba el host
// - Actualizaciones sin reiniciar (el subproceso se puede reemplazar)
// - Seguridad: el plugin corre con permisos propios

// Desventajas:
// - Mayor latencia (comunicación entre procesos)
// - Más complejidad de configuración

Usa el paquete plugin nativo cuando necesites latencia mínima y controlas el entorno de ejecución (Linux/macOS, versión Go fija). Usa hashicorp/go-plugin cuando necesites portabilidad, aislamiento de fallos o soporte en Windows.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP