Go’s plugin system is surprisingly flexible, letting you load code after your main application has already started, without recompiling anything.

Let’s see this in action. Imagine we have a simple calculator app that needs to support various operations. Instead of hardcoding "add" and "subtract," we want to load them as plugins.

First, define the interface that all our plugins must implement. This is the contract they’ll adhere to.

// calculator.go
package main

import "fmt"

// Operation defines the interface for any calculator operation.
type Operation interface {
	Name() string
	Execute(a, b int) int
}

func main() {
	// In a real app, you'd load plugins dynamically here.
	// For this example, we'll simulate it.
	operations := []Operation{
		&addOperation{}, // Simulate loading add plugin
		&subtractOperation{}, // Simulate loading subtract plugin
	}

	fmt.Println("Performing calculations:")
	for _, op := range operations {
		result := op.Execute(10, 5)
		fmt.Printf("%s: 10 op 5 = %d\n", op.Name(), result)
	}
}

// --- Simulate Plugin Implementations ---

type addOperation struct{}

func (a *addOperation) Name() string {
	return "add"
}

func (a *addOperation) Execute(a, b int) int {
	return a + b
}

type subtractOperation struct{}

func (s *subtractOperation) Name() string {
	return "subtract"
}

func (s *subtractOperation) Execute(a, b int) int {
	return a - b
}

This calculator.go file is our main application. It defines the Operation interface and has a main function that uses it. The addOperation and subtractOperation structs are our simulated plugins.

Now, let’s build the actual plugins. Each plugin will be a separate Go package that implements the Operation interface.

// addplugin/add.go
package main

import (
	"calculator" // Assuming calculator package is accessible
)

// AddOperation implements the calculator.Operation interface.
type AddOperation struct{}

func (a *AddOperation) Name() string {
	return "add"
}

func (a *AddOperation) Execute(a, b int) int {
	return a + b
}

// The plugin must export a variable of the interface type.
var Plugin AddOperation

func main() {
	// This main function is required for the plugin to be built as an executable.
	// It's not directly called when the plugin is loaded by the host application.
}
// subtractplugin/subtract.go
package main

import (
	"calculator" // Assuming calculator package is accessible
)

// SubtractOperation implements the calculator.Operation interface.
type SubtractOperation struct{}

func (s *SubtractOperation) Name() string {
	return "subtract"
}

func (s *SubtractOperation) Execute(a, b int) int {
	return a - b
}

// The plugin must export a variable of the interface type.
var Plugin SubtractOperation

func main() {
	// Required for plugin build.
}

To build these plugins, you’d compile them as shared libraries (.so on Linux, .dylib on macOS, .dll on Windows). Crucially, they need to be compiled with the same Go version as the main application.

# Make sure you have the calculator package accessible (e.g., in your GOPATH or module path)
go build -buildmode=plugin -o add.so addplugin/add.go
go build -buildmode=plugin -o subtract.so subtractplugin/subtract.go

Now, let’s modify our calculator.go to actually load these plugins at runtime.

// calculator.go (modified)
package main

import (
	"fmt"
	"plugin"
	"log"
)

// Operation defines the interface for any calculator operation.
type Operation interface {
	Name() string
	Execute(a, b int) int
}

func main() {
	// Paths to our compiled plugins
	pluginPaths := []string{"./add.so", "./subtract.so"}
	operations := make(map[string]Operation)

	fmt.Println("Loading plugins...")
	for _, pPath := range pluginPaths {
		// Open the plugin
		p, err := plugin.Open(pPath)
		if err != nil {
			log.Fatalf("Failed to open plugin %s: %v", pPath, err)
		}

		// Look up the symbol (our exported variable)
		sym, err := p.Lookup("Plugin")
		if err != nil {
			log.Fatalf("Failed to lookup symbol 'Plugin' in %s: %v", pPath, err)
		}

		// Assert that the symbol is of our Operation interface type
		op, ok := sym.(Operation)
		if !ok {
			log.Fatalf("Symbol 'Plugin' in %s is not of type Operation", pPath)
		}

		// Store the loaded operation
		operations[op.Name()] = op
		fmt.Printf("Loaded plugin: %s\n", op.Name())
	}

	fmt.Println("\nPerforming calculations:")
	// Use the loaded operations
	if addOp, ok := operations["add"]; ok {
		result := addOp.Execute(10, 5)
		fmt.Printf("add: 10 + 5 = %d\n", result)
	} else {
		fmt.Println("Add operation not found.")
	}

	if subOp, ok := operations["subtract"]; ok {
		result := subOp.Execute(10, 5)
		fmt.Printf("subtract: 10 - 5 = %d\n", result)
	} else {
		fmt.Println("Subtract operation not found.")
	}
}

When you run the modified calculator.go (after building the plugins), it will dynamically load add.so and subtract.so, find the exported Plugin variable in each, cast it to the Operation interface, and then use it.

The core mechanism here is plugin.Open and plugin.Lookup. plugin.Open loads the shared library into the running process. plugin.Lookup then searches for an exported symbol (a variable, function, type, etc.) by its name. The crucial part is that the symbol must be of a type compatible with the interface defined in the main application. This is why exporting a variable of the interface type (like var Plugin AddOperation) is key. The runtime can then verify that the loaded code provides an implementation that satisfies the Operation interface.

One subtlety that trips people up is that the plugin must be compiled with the exact same Go version as the host application. If there’s even a minor version difference, you’ll likely get a runtime panic when trying to load or use the plugin, often with a cryptic "plugin was built with a different version of runtime" error. This is because the internal representations of types and the runtime system need to be identical for dynamic loading to work.

The next hurdle you’ll face is managing dependencies between plugins or between plugins and the main application, especially when dealing with non-interface types.

Want structured learning?

Take the full Golang course →