One of the most underappreciated features introduced since Go version 1.8 is the Go plugin package. Plugins are one of the many software architectural designs that allow you to build loosely coupled and modular programs. In Go, plugins are written and compiled separately as shared objects (.so) or libraries and can be loaded dynamically during runtime. 

Incorporating extensibility into your programs is always considered a good practice, and there are many approaches to this. However, the current plugin package in Go does come with its fair share of headaches. Below is a side-by-side table listing the Go plugins’ pros and cons in its current state.

Pros
  • Go plugins give you a tool to help you adopt the single responsibility principle and separation of concerns.
  • It helps to break down your code into small manageable and reusable components.
  • It gives you a way to load plugins dynamically during application runtime without recompiling the program.
Cons
  • The environment for building a Go plugin such as OS, Go language version, and dependency versions must match exactly.
  • As of now, unloading plugins are not allowed without restarting the program.
  • You cannot replace a plugin with a newer version during runtime; that’s because Go doesn’t currently support unloading plugins.
  • As of Go v1.11, you can only build plugins on Linux, FreeBSD, and Mac.
Example Program: Shipping Calculator

To demonstrate how to develop plugins in Go, We’ll create a bare minimum and admittedly impractical example of a shipping calculator. The example though impractical will be useful to understand how plugins work in Go.

The basic shipping calculator will give you rates based on which shipping method and parcel weight you provide. You can support different shipping methods by adding new plugins, and the calculator will produce the rates and currency based on your preferred shipping method. The interface for a shipping method contains three functions, GetCurrency,  CalculateRate, and Name.

Let’s get to coding!

Development Environment and Packages
  • Go 1.15 with Go modules
  • GoLand IDE (vscode or any text editor will work fine)
  • tablewriter v0.0.4
Create and Initialize the Project using Go modules
mkdir go-plugins-shipping-calculator

cd go-plugins-shipping-calculator

go mod init go-plugins-shipping-calculator

go get github.com/olekukonko/[email protected]
The Main Application Entrypoint
package main

import (
	"fmt"
	"github.com/olekukonko/tablewriter"
	"log"
	"os"
	"plugin"
	"strconv"
)

type Shipper interface {
	Name() string
	Currency() string
	CalculateRate(weight float32) float32
}

func main() {
	args := os.Args[1:]
	if len(args) == 2 {
		pluginName := args[0]
		weight, _ := strconv.ParseFloat(args[1], 32)

		// Load the plugin
		// 1. Search the plugins directory for a file with the same name as the pluginName
		// that was passed in as an argument and attempt to load the shared object file.
		plug, err := plugin.Open(fmt.Sprintf("plugins/%s.so", pluginName))
		if err != nil {
			log.Fatal(err)
		}

		// 2. Look for an exported symbol such as a function or variable
		// in our case we expect that every plugin will have exported a single struct
		// that implements the Shipper interface with the name "Shipper"
		shipperSymbol, err := plug.Lookup("Shipper")

		if err != nil {
			log.Fatal(err)
		}

		// 3. Attempt to cast the symbol to the Shipper
		// this will allow us to call the methods on the plugins if the plugin
		// implemented the required methods or fail if it does not implement it.
		var shipper Shipper
		shipper, ok := shipperSymbol.(Shipper)

		if !ok {
			log.Fatal("Invalid shipper type")
		}

		// 4. If everything is ok from the previous assertions, then we can proceed
		// with calling the methods on our shipper interface object
		rate := shipper.CalculateRate(float32(weight))
		rate1Day := fmt.Sprintf("%.2f %s", rate, shipper.Currency())

		rate2Days := fmt.Sprintf("%.2f %s",
			rate - (rate * .20),
			shipper.Currency())

		rate7Days := fmt.Sprintf("%.2f %s",
			rate - (rate * .70),
			shipper.Currency())

		table := tablewriter.NewWriter(os.Stdout)

		fmt.Println(shipper.Name())
		table.SetHeader([]string{"Number of Days", "Rate"})

		table.Append([]string{"1 Day Express", rate1Day})
		table.Append([]string{"2 Days Shipping", rate2Days})
		table.Append([]string{"7 Days Shipping", rate7Days})
		table.Render()
	}
}
Plugins
Fedex Shipper Implementation
// file: fedex/fedex.go

package main

type shipper struct {}

func (s shipper) Name() string {
	return "Federal Express (Fedex)"
}

func (s shipper) Currency() string {
	return "USD"
}

func (s shipper) CalculateRate(weight float32) float32 {
	cost := weight * 1.8
	tax := cost * .10

	return cost + tax
}

var Shipper shipper
Royal Mail Shipper Implementation
# file: royalmail/royalmail.go

package main

type shipper struct {}

func (s shipper) Name() string {
	return "Royal Mail (RM)"
}

func (s shipper) Currency() string {
	return "GBP"
}

func (s shipper) CalculateRate(weight float32) float32 {
	cost := weight * .9
	tax := cost * .5

	return cost + tax
}

var Shipper shipper

Check out my upcoming tutorial Creating Unified Development Environments With Docker, to tackle the challenge of compiling Go plugins in the same environment. And my other upcoming tutorial Zero Downtime Go Plugin Reload, to ease the pain of restarting your program when new versions of your plugin are released.

Until then, happy coding!

Newsletter

Categories

Tags

Login

Join the conversation
Registration is closed.