How to Use hashicorp/go-plugin for Plugin Systems

Use `hashicorp/go-plugin` to create a plugin system by defining a shared interface, implementing it in a plugin binary, and using the `plugin.Serve` and `plugin.Client` functions to handle the gRPC handshake and protocol negotiation.

Use hashicorp/go-plugin to create a plugin system by defining a shared interface, implementing it in a plugin binary, and using the plugin.Serve and plugin.Client functions to handle the gRPC handshake and protocol negotiation. The library automatically manages the plugin process lifecycle, serialization, and remote procedure calls, allowing your host application to load and call plugin methods as if they were local.

First, define a shared interface in a package that both the host and the plugin import. This interface acts as the contract for your plugin system.

// plugin_interface.go
package plugin

import "context"

// Greeter is the interface that both host and plugin must implement.
type Greeter interface {
    Greet(ctx context.Context, name string) (string, error)
}

// Plugin is the interface that the plugin must implement to be recognized.
type Plugin interface {
    Server(*plugin.ServeConfig) interface{}
}

Next, create the plugin binary. It registers the interface implementation with the plugin system and starts the gRPC server.

// plugin_main.go
package main

import (
    "context"
    "os"
    "github.com/hashicorp/go-plugin"
    "your/module/plugin" // The shared interface package
)

// MyGreeter implements the Greeter interface.
type MyGreeter struct{}

func (g *MyGreeter) Greet(ctx context.Context, name string) (string, error) {
    return "Hello, " + name + " from the plugin!", nil
}

func main() {
    plugin.Serve(&plugin.ServeConfig{
        HandshakeConfig: plugin.HandshakeConfig{
            ProtocolVersion:  1,
            MagicCookieKey:   "PLUGIN_MAGIC",
            MagicCookieValue: "secret",
        },
        Plugins: map[string]plugin.Plugin{
            "greeter": &plugin.Plugin{
                GRPCPlugin: &plugin.GRPCPlugin{
                    Server: func() interface{} { return &MyGreeter{} },
                },
            },
        },
    })
}

Finally, in your host application, use plugin.Client to spawn the plugin process, establish the connection, and retrieve the interface implementation.

// host_main.go
package main

import (
    "context"
    "log"
    "github.com/hashicorp/go-plugin"
    "your/module/plugin"
)

func main() {
    // Create a client to the plugin
    client := plugin.NewClient(&plugin.ClientConfig{
        HandshakeConfig: plugin.HandshakeConfig{
            ProtocolVersion:  1,
            MagicCookieKey:   "PLUGIN_MAGIC",
            MagicCookieValue: "secret",
        },
        Plugins: map[string]plugin.Plugin{
            "greeter": &plugin.Plugin{
                GRPCPlugin: &plugin.GRPCPlugin{
                    Client: func() interface{} { return &plugin.GreeterClient{} },
                },
            },
        },
        Cmd: exec.Command("./my-plugin-binary"),
    })

    // Connect to the plugin
    rpcClient, err := client.Client()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Kill()

    // Request the plugin interface
    raw, err := rpcClient.Dispense("greeter")
    if err != nil {
        log.Fatal(err)
    }

    greeter := raw.(plugin.Greeter)
    msg, err := greeter.Greet(context.Background(), "World")
    if err != nil {
        log.Fatal(err)
    }
    log.Println(msg)
}

Ensure both the host and plugin binaries are built with the same go.mod dependencies to avoid version mismatches in the shared interface. The library handles the complex gRPC setup, so you only need to focus on implementing the business logic within your interface methods.