Exploring the plugin package

August 24, 2018

A while ago, I wanted to test how I could integrate the plugin package in a project I’m working on.

As you probably know, the plugin package is a way to load Go code from dynamic libraries. This allows you to extend and app with more code without having to recompile the main application.

Before we continue, the first thing you need to know is that Go currently only supports plugins on Linux and Mac, not on Windows.

The first step in using plugins is to define an interface to which the plugins need to conform. This is needed to ensure we can properly load the plugin.

For this example, we define the following interface:

type PluginGreeter interface {
    Greet()
}

For each plugin we want to test, we then need to create a main package which implement the interface. This is a sample implementation:

package main

import (
    "fmt"

    "plugin01/uuid"
)

type greeting string

func (g greeting) Greet() {
    fmt.Println("Hello Universe from plugin 1 - " + uuid.UUID())
}

// exported as symbol named "Greeter"
var Greeter greeting

Another plugin might do something different for the greeting:

package main

import (
    "fmt"
)

type greeting string

func (g greeting) Greet() {
    fmt.Println("Hello Universe from plugin 2")
}

// exported as symbol named "Greeter"
var Greeter greeting

This then needs to be compiled before we can use it. To compile a plugin, you need to specify plugin as the buildmode. This is passed as an argument to the go build command:

go build -buildmode=plugin -o plugin01.so plugin01/main

The result of this step is a .so file which can be dynamically loaded from another Go application.

Loading a plugin is done by means of the plugin package. Given that we know the path of the plugin we want to load, we can load it as follows:

package main

import (
    "errors"
    "path/filepath"
    "plugin"
)

func loadPluginAndExecute(path string) error {

    // Load the plugin from the file
    plug, err := plugin.Open(path)
    if err != nil {
        return err
    }

    // Lookup the symbol called "Greeter"
    symGreeter, err := plug.Lookup("Greeter")
    if err != nil {
        return err
    }

    // Cast the symbol to the PluginGreeter interface
    var greeter PluginGreeter
    greeter, ok := symGreeter.(PluginGreeter)
    if !ok {
        return errors.New("Unexpected type from module symbol")
    }

    // Perform the Greet function
    greeter.Greet()

    return nil

}

As you can see, using a plugin is pretty straightforward, but it takes some effort to get the project setup done.

What you should remember is that the .so files are compiled the same way as executables, so if you build them on a mac, they will not work on a Linux box. You can get around this by using cross compilation.

In the sample source code you can download here, I’ve provided a Makefile to make the whole setup a bit easier. It also has support for using dep for managing the dependencies.

You can use the following make targets:

  • make build: builds the main app and the two plugins
  • make run: builds and runs the main app
  • make clean: removes the build files
  • make dep-init: performs dep init for the main app and the two plugins
  • make dep-ensure: performs dep ensure for the main app and the two plugins

I also added a Visual Studio Code tasks.json file so you can run these straight from your editor.