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.
