#development #golang #pattern

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:

1type PluginGreeter interface {
2    Greet()
3}

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

 1package main
 2
 3import (
 4    "fmt"
 5
 6    "plugin01/uuid"
 7)
 8
 9type greeting string
10
11func (g greeting) Greet() {
12    fmt.Println("Hello Universe from plugin 1 - " + uuid.UUID())
13}
14
15// exported as symbol named "Greeter"
16var Greeter greeting

Another plugin might do something different for the greeting:

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7type greeting string
 8
 9func (g greeting) Greet() {
10    fmt.Println("Hello Universe from plugin 2")
11}
12
13// exported as symbol named "Greeter"
14var 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:

1go 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:

 1package main
 2
 3import (
 4    "errors"
 5    "path/filepath"
 6    "plugin"
 7)
 8
 9func loadPluginAndExecute(path string) error {
10
11    // Load the plugin from the file
12    plug, err := plugin.Open(path)
13    if err != nil {
14        return err
15    }
16
17    // Lookup the symbol called "Greeter"
18    symGreeter, err := plug.Lookup("Greeter")
19    if err != nil {
20        return err
21    }
22
23    // Cast the symbol to the PluginGreeter interface
24    var greeter PluginGreeter
25    greeter, ok := symGreeter.(PluginGreeter)
26    if !ok {
27        return errors.New("Unexpected type from module symbol")
28    }
29
30    // Perform the Greet function
31    greeter.Greet()
32
33    return nil
34
35}

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.