How to Build Extensible Go Applications With Plugins

Mani Muridi
The Startup
Published in
4 min readOct 21, 2020

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-calculatorcd go-plugins-shipping-calculatorgo mod init go-plugins-shipping-calculatorgo get github.com/olekukonko/tablewriter@v0.0.4

The Main Application Entrypoint

package mainimport (
"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.gopackage maintype 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.gopackage maintype 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

Thanks for reading and happy coding!

Source Code: https://github.com/ManiMuridi/go-plugins-shipping-calculator

--

--

Mani Muridi
The Startup

A Software Craftsman/Architect with over 13 years of experience engineering robust solutions and has a knack for DevOps and Microservices. blog.manimuridi.com