Plugin Architecture in Go

Why Plugin Architecture Matters

💡 Key Takeaway: Think of plugin architecture like a smartphone app store. Instead of building every feature into the phone, you provide a platform where developers can create apps that users can install and uninstall as needed. This creates powerful ecosystems.

Plugin architectures enable ecosystems—allowing third-party extensions without modifying core code. Understanding plugins separates monolithic applications from extensible platforms like Docker, Kubernetes, and HashiCorp tools.

Real-World Impact:

Docker Plugins - 200+ community plugins:

  • Storage plugins: AWS EBS, Azure Disk, GlusterFS
  • Network plugins: Weave, Calico, Flannel
  • Impact: Docker extensible without core changes

Kubernetes Operators - 100+ production operators:

  • Database operators
  • ML operators
  • Pattern: Plugin architecture via CRDs + controllers

HashiCorp's go-plugin - Powers Terraform/Vault/Packer:

  • 1000+ Terraform providers
  • RPC-based, crash-isolated plugins
  • Impact: Enables massive ecosystem without core complexity

Real-world Example: Web Browser Extensions
Your browser doesn't include every possible feature. Instead, it provides extension points where developers can add:

  • Ad blockers
  • Password managers
  • Developer tools
  • Theme changers
    This keeps the browser lightweight while allowing endless customization.

Learning Objectives

By the end of this tutorial, you will master:

Core Concepts:

  • Plugin architecture patterns and trade-offs
  • Go's native plugin system capabilities and limitations
  • RPC-based plugin communication protocols
  • Security and isolation strategies for plugins

Practical Skills:

  • Building dynamic plugin loading systems
  • Implementing hot-reloadable plugin architectures
  • Creating secure plugin sandboxes
  • Managing plugin lifecycle and versioning

Production Patterns:

  • HashiCorp go-plugin pattern implementation
  • WebAssembly plugin sandboxing
  • Plugin discovery and registry systems
  • Multi-protocol plugin management

Core Concepts

What is Plugin Architecture?

Plugin architectures allow applications to be extended with new functionality at runtime without recompiling the main application. This design pattern is particularly useful for building extensible systems where third-party developers can add features, or where you want to enable/disable features dynamically.

⚠️ Important: Plugin architecture is different from microservices. With plugins, extensions run within your application process. With microservices, you have independent services communicating over networks.

Plugin Types in Go

Go provides multiple approaches to plugin architecture, each with different characteristics:

Native Plugin Package:

  • Dynamic loading of Go plugins
  • In-process execution for maximum performance
  • Platform limitations

RPC-Based Plugins:

  • Communication over network protocols
  • Process isolation and security
  • Cross-platform compatibility

WebAssembly Plugins:

  • Running plugins in sandboxed environment
  • Maximum security and portability
  • Language-agnostic plugin development

Embedded Scripting:

  • Using languages like Lua or JavaScript
  • Runtime script execution
  • Fast iteration and development

Each approach has trade-offs in terms of safety, performance, and ease of use. Throughout this tutorial, we'll explore these different approaches and learn when to use each one.

Why Use Plugin Architectures?

  • Extensibility: Allow third-party extensions without source code access
  • Modularity: Separate optional features from core functionality
  • Runtime loading: Add/remove features without restarting
  • Versioning: Load different versions of plugins side by side
  • Isolation: Keep untrusted code separate from the main application

Common Pitfalls:

  • ❌ Security vulnerabilities from untrusted plugins
  • ❌ Performance overhead from plugin boundaries
  • ❌ Complex debugging across process boundaries
  • ❌ Version compatibility issues

Practical Examples

Getting Started with Native Go Plugins

Let's start with the simplest and most direct approach to plugin architecture in Go. The native plugin package allows you to load Go code compiled as shared libraries.

Real-world Example: Think of this like loading LEGOs - each plugin is a self-contained piece you can snap into your creation.

 1// First, create a simple plugin
 2// File: plugin.go
 3package main
 4
 5import "fmt"
 6
 7// Exported variable - accessible from host
 8var Version = "1.0.0"
 9
10// Exported function - accessible from host
11func Greet(name string) string {
12    return fmt.Sprintf("Hello, %s!", name)
13}
14
15// Plugins must have a main function
16func main() {}

Building the Plugin:

1# Build as shared library
2go build -buildmode=plugin -o greeter.so plugin.go

Main Application that Loads Plugin:

 1// File: main.go
 2package main
 3
 4import (
 5    "fmt"
 6    "log"
 7    "plugin"
 8)
 9
10func main() {
11    // Open the plugin
12    p, err := plugin.Open("greeter.so")
13    if err != nil {
14        log.Fatal("Failed to load plugin:", err)
15    }
16
17    // Lookup exported variable
18    versionSym, err := p.Lookup("Version")
19    if err != nil {
20        log.Fatal("Failed to find Version:", err)
21    }
22
23    // Type assert the symbol to expected type
24    version := versionSym.(*string)
25    fmt.Printf("Plugin version: %s\n", *version)
26
27    // Lookup exported function
28    greetSym, err := p.Lookup("Greet")
29    if err != nil {
30        log.Fatal("Failed to find Greet function:", err)
31    }
32
33    // Type assert to function signature
34    greet := greetSym.(func(string) string)
35    message := greet("World")
36    fmt.Println(message)
37}
38
39// Output:
40// Plugin version: 1.0.0
41// Hello, World!

Key Points:

  • Only exported symbols can be looked up
  • The plugin must have a main() function, even if empty
  • Type assertions are required to convert symbols to correct types

Interface-Based Plugin System

Interfaces provide better type safety and cleaner API design. Think of interfaces like USB standards - any device that follows the USB spec can plug into any USB port.

 1// File: shared/interface.go
 2package shared
 3
 4// Define plugin interface that all plugins must implement
 5type Processor interface {
 6    Process(data string)
 7    Name() string
 8}
 9
10// File: plugins/uppercase/plugin.go
11package main
12
13import (
14    "strings"
15    "yourapp/shared"
16)
17
18type UppercaseProcessor struct{}
19
20func Process(data string) {
21    return strings.ToUpper(data), nil
22}
23
24func Name() string {
25    return "Uppercase Processor"
26}
27
28// Exported symbol that main app will lookup
29var Processor shared.Processor = &UppercaseProcessor{}
30
31func main() {}

Building and Using Interface Plugin:

1go build -buildmode=plugin -o uppercase.so plugins/uppercase/plugin.go
 1// File: main.go
 2package main
 3
 4import (
 5    "fmt"
 6    "log"
 7    "plugin"
 8    "yourapp/shared"
 9)
10
11func main() {
12    p, err := plugin.Open("uppercase.so")
13    if err != nil {
14        log.Fatal(err)
15    }
16
17    // Lookup the Processor symbol
18    processorSym, err := p.Lookup("Processor")
19    if err != nil {
20        log.Fatal(err)
21    }
22
23    // Type assert to interface
24    processor := processorSym.(shared.Processor)
25
26    fmt.Printf("Loaded plugin: %s\n", processor.Name())
27
28    result, err := processor.Process("hello world")
29    if err != nil {
30        log.Fatal(err)
31    }
32
33    fmt.Printf("Result: %s\n", result)
34}
35
36// Output:
37// Loaded plugin: Uppercase Processor
38// Result: HELLO WORLD

Discovering and Loading Multiple Plugins

Most real applications need to discover and load multiple plugins automatically. Here's how to build a plugin registry:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "path/filepath"
 7    "plugin"
 8    "sync"
 9    "yourapp/shared"
10)
11
12type PluginRegistry struct {
13    mu      sync.RWMutex
14    plugins map[string]shared.Processor
15}
16
17func NewPluginRegistry() *PluginRegistry {
18    return &PluginRegistry{
19        plugins: make(map[string]shared.Processor),
20    }
21}
22
23func LoadFromDirectory(dir string) error {
24    // Find all .so files in directory
25    matches, err := filepath.Glob(filepath.Join(dir, "*.so"))
26    if err != nil {
27        return err
28    }
29
30    for _, path := range matches {
31        if err := r.loadPlugin(path); err != nil {
32            log.Printf("Failed to load plugin %s: %v", path, err)
33            continue
34        }
35    }
36
37    return nil
38}
39
40func loadPlugin(path string) error {
41    p, err := plugin.Open(path)
42    if err != nil {
43        return fmt.Errorf("open plugin: %w", err)
44    }
45
46    // Look for Processor symbol
47    sym, err := p.Lookup("Processor")
48    if err != nil {
49        return fmt.Errorf("lookup Processor: %w", err)
50    }
51
52    // Type assert to our interface
53    processor, ok := sym.(shared.Processor)
54    if !ok {
55        return fmt.Errorf("invalid plugin type")
56    }
57
58    // Register the plugin
59    r.mu.Lock()
60    r.plugins[processor.Name()] = processor
61    r.mu.Unlock()
62
63    log.Printf("Loaded plugin: %s", processor.Name())
64    return nil
65}
66
67func Get(name string) {
68    r.mu.RLock()
69    defer r.mu.RUnlock()
70
71    p, ok := r.plugins[name]
72    return p, ok
73}
74
75func ProcessWithAll(data string) {
76    r.mu.RLock()
77    defer r.mu.RUnlock()
78
79    for name, processor := range r.plugins {
80        result, err := processor.Process(data)
81        if err != nil {
82            log.Printf("Plugin %s failed: %v", name, err)
83            continue
84        }
85        fmt.Printf("%s: %s\n", name, result)
86    }
87}
88
89func main() {
90    registry := NewPluginRegistry()
91
92    // Load all plugins from ./plugins directory
93    if err := registry.LoadFromDirectory("./plugins"); err != nil {
94        log.Printf("Warning: %v", err)
95    }
96
97    // Process data with all loaded plugins
98    registry.ProcessWithAll("Hello, Plugin World!")
99}

The Go Plugin Package - Advanced Topics

The plugin package enables loading of Go plugins compiled as shared libraries. This is the most straightforward approach for Go-to-Go plugins. Think of it like loading LEGO bricks - each plugin is a self-contained piece you can snap into your creation.

Basic Plugin Loading

Real-world Example: Photoshop Filters
Photoshop doesn't include every possible filter. Instead, it loads filter plugins that implement a standard interface. Each filter is a self-contained unit that can process images.

Plugin Code:

 1package main
 2
 3import "fmt"
 4
 5// Exported variable
 6var Version = "1.0.0"
 7
 8// Exported function
 9func Greet(name string) string {
10    return fmt.Sprintf("Hello, %s!", name)
11}
12
13// Must have empty main
14func main() {}

Building the Plugin:

1go build -buildmode=plugin -o greeter.so plugin.go

Main Application:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "plugin"
 7)
 8
 9func main() {
10    // Open the plugin
11    p, err := plugin.Open("greeter.so")
12    if err != nil {
13        log.Fatal(err)
14    }
15
16    // Lookup a symbol
17    versionSym, err := p.Lookup("Version")
18    if err != nil {
19        log.Fatal(err)
20    }
21
22    // Type assert the symbol
23    version := versionSym.(*string)
24    fmt.Printf("Plugin version: %s\n", *version)
25
26    // Lookup a function
27    greetSym, err := p.Lookup("Greet")
28    if err != nil {
29        log.Fatal(err)
30    }
31
32    // Type assert to function signature
33    greet := greetSym.(func(string) string)
34    message := greet("World")
35    fmt.Println(message)
36}
37
38// Output:
39// Plugin version: 1.0.0
40// Hello, World!

💡 Key Takeaway: The plugin package lets you load Go code at runtime, but it comes with significant limitations - it only works on Unix-like systems and requires exact Go version matching.

⚠️ Important: Only exported symbols can be looked up. The plugin must have a main() function, even if empty.

Common Pitfalls:

  • ❌ Trying to load plugins on Windows
  • ❌ Building plugins with different Go versions
  • ❌ Forgetting to export symbols
  • ❌ Complex data structures across plugin boundaries

Plugin with Interfaces

Using interfaces provides better type safety and cleaner API design. Think of interfaces like USB standards - any device that follows the USB spec can plug into any USB port.

Real-world Example: Audio Equipment
All microphones follow the same XLR interface standard. You can plug any brand microphone into any mixer or preamp that has XLR inputs. The interface defines the contract, not the implementation.

Why This Works: By defining a shared interface, both the host application and plugins agree on a contract. The host doesn't need to know implementation details—it only needs to know the interface. This enables true polymorphism where plugins can be swapped without modifying the host code.

Interface Definition:

1// shared/plugin.go
2package shared
3
4type Processor interface {
5    Process(data string)
6    Name() string
7}

Plugin Implementation:

 1// plugins/uppercase/plugin.go
 2package main
 3
 4import (
 5    "strings"
 6    "yourapp/shared"
 7)
 8
 9type UppercaseProcessor struct{}
10
11func Process(data string) {
12    return strings.ToUpper(data), nil
13}
14
15func Name() string {
16    return "Uppercase Processor"
17}
18
19// Exported symbol that main app will lookup
20var Processor shared.Processor = &UppercaseProcessor{}
21
22func main() {}

Building:

1go build -buildmode=plugin -o uppercase.so plugins/uppercase/plugin.go

Loading and Using:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "plugin"
 7    "yourapp/shared"
 8)
 9
10func main() {
11    p, err := plugin.Open("uppercase.so")
12    if err != nil {
13        log.Fatal(err)
14    }
15
16    // Lookup the Processor symbol
17    processorSym, err := p.Lookup("Processor")
18    if err != nil {
19        log.Fatal(err)
20    }
21
22    // Type assert to interface
23    processor := processorSym.(shared.Processor)
24
25    fmt.Printf("Loaded plugin: %s\n", processor.Name())
26
27    result, err := processor.Process("hello world")
28    if err != nil {
29        log.Fatal(err)
30    }
31
32    fmt.Printf("Result: %s\n", result)
33}
34
35// Output:
36// Loaded plugin: Uppercase Processor
37// Result: HELLO WORLD

💡 Key Takeaway: Interfaces create a contract between plugins and hosts. The host doesn't care about implementation details, only that the plugin fulfills the interface contract.

When to Use Interface-Based Plugins:

  • ✅ Multiple plugins implementing the same functionality
  • ✅ Need to swap implementations at runtime
  • ✅ Third-party developers building plugins
  • ✅ Complex APIs with multiple methods

Common Pitfalls:

  • ❌ Interface that's too large
  • ❌ Breaking interface changes
  • ❌ Type assertion panics when plugins don't implement interfaces correctly

Plugin Discovery and Loading

A robust plugin system needs automatic plugin discovery and loading mechanisms. Think of this like an app store that automatically discovers and installs compatible apps.

Real-world Example: IDE Extensions
Visual Studio Code automatically scans extension directories, validates extensions, and makes them available. Users see a list of installed extensions and can enable/disable them without restarting the IDE.

Plugin Registry Pattern

  1package main
  2
  3import (
  4    "fmt"
  5    "log"
  6    "path/filepath"
  7    "plugin"
  8    "sync"
  9)
 10
 11type Plugin interface {
 12    Name() string
 13    Version() string
 14    Execute(args []string) error
 15}
 16
 17type PluginRegistry struct {
 18    mu      sync.RWMutex
 19    plugins map[string]Plugin
 20}
 21
 22func NewPluginRegistry() *PluginRegistry {
 23    return &PluginRegistry{
 24        plugins: make(map[string]Plugin),
 25    }
 26}
 27
 28func Register(name string, p Plugin) error {
 29    r.mu.Lock()
 30    defer r.mu.Unlock()
 31
 32    if _, exists := r.plugins[name]; exists {
 33        return fmt.Errorf("plugin %s already registered", name)
 34    }
 35
 36    r.plugins[name] = p
 37    return nil
 38}
 39
 40func Get(name string) {
 41    r.mu.RLock()
 42    defer r.mu.RUnlock()
 43
 44    p, ok := r.plugins[name]
 45    return p, ok
 46}
 47
 48func List() []string {
 49    r.mu.RLock()
 50    defer r.mu.RUnlock()
 51
 52    names := make([]string, 0, len(r.plugins))
 53    for name := range r.plugins {
 54        names = append(names, name)
 55    }
 56    return names
 57}
 58
 59func LoadFromDirectory(dir string) error {
 60    // Find all .so files
 61    matches, err := filepath.Glob(filepath.Join(dir, "*.so"))
 62    if err != nil {
 63        return err
 64    }
 65
 66    for _, path := range matches {
 67        if err := r.LoadPlugin(path); err != nil {
 68            log.Printf("Failed to load plugin %s: %v", path, err)
 69            continue
 70        }
 71    }
 72
 73    return nil
 74}
 75
 76func LoadPlugin(path string) error {
 77    p, err := plugin.Open(path)
 78    if err != nil {
 79        return fmt.Errorf("open plugin: %w", err)
 80    }
 81
 82    // Lookup the Plugin symbol
 83    sym, err := p.Lookup("Plugin")
 84    if err != nil {
 85        return fmt.Errorf("lookup Plugin: %w", err)
 86    }
 87
 88    // Type assert to Plugin interface
 89    pluginInstance, ok := sym.(Plugin)
 90    if !ok {
 91        return fmt.Errorf("invalid plugin type")
 92    }
 93
 94    // Register the plugin
 95    return r.Register(pluginInstance.Name(), pluginInstance)
 96}
 97
 98func main() {
 99    registry := NewPluginRegistry()
100
101    // Load all plugins from directory
102    if err := registry.LoadFromDirectory("./plugins"); err != nil {
103        log.Printf("Warning: %v", err)
104    }
105
106    // List loaded plugins
107    fmt.Println("Loaded plugins:")
108    for _, name := range registry.List() {
109        p, _ := registry.Get(name)
110        fmt.Printf("  - %s\n", p.Name(), p.Version())
111    }
112
113    // Execute a specific plugin
114    if p, ok := registry.Get("example-plugin"); ok {
115        if err := p.Execute([]string{"arg1", "arg2"}); err != nil {
116            log.Printf("Plugin execution failed: %v", err)
117        }
118    }
119}

💡 Key Takeaway: A plugin registry provides a centralized way to manage plugins - discover them, load them, and make them available to the rest of your application.

Benefits of Plugin Registry:

  • ✅ Automatic discovery of plugins
  • ✅ Centralized plugin management
  • ✅ Thread-safe access to plugins
  • ✅ Error isolation

Common Pitfalls:

  • ❌ Loading plugins without validation
  • ❌ No version compatibility checking
  • ❌ Memory leaks from plugins that can't be unloaded
  • ❌ Race conditions when plugins access shared resources

When to Use Plugin Registry:

  • ✅ Applications with many plugins
  • ✅ Need to load/unload plugins dynamically
  • ✅ Multiple parts of application need access to plugins
  • ✅ Need to track plugin metadata

Plugin Limitations and Caveats

The Go plugin package has several important limitations that you need to understand before using it in production.

⚠️ Important: These limitations are why many production systems choose RPC-based plugins over native Go plugins.

1. Platform Limitations

Real-world Problem: Imagine you build a universal remote that only works with Samsung TVs. Customers with LG, Sony, or other brands can't use it. That's the Go plugin package on Windows.

 1// Plugin only works on Linux, FreeBSD, and macOS
 2// Windows is NOT supported
 3
 4// +build linux freebsd darwin
 5
 6package main
 7
 8import "plugin"
 9
10func loadPlugin(path string) error {
11    p, err := plugin.Open(path)
12    if err != nil {
13        return err
14    }
15    // Use plugin...
16    return nil
17}

Impact: 30% of desktop users can't use your plugin system. This is a dealbreaker for consumer applications.

2. Version Compatibility

Real-world Example: Car Parts
You can't put a Toyota engine in a Honda car - even though both are engines, the mounts, connectors, and electronics don't match. Go plugins have the same problem.

Plugins must be built with the exact same Go version and dependencies as the main application.

Why This Matters: Go's plugin system uses shared library linking, which requires ABI compatibility. Different Go versions or dependency versions can change struct layouts, function signatures, or internal representations, causing crashes or undefined behavior at runtime.

 1// This will fail if versions don't match
 2package main
 3
 4import (
 5    "fmt"
 6    "log"
 7    "plugin"
 8    "runtime"
 9)
10
11func loadPluginSafely(path string) error {
12    // Check Go version
13    expectedVersion := runtime.Version()
14
15    p, err := plugin.Open(path)
16    if err != nil {
17        return fmt.Errorf("plugin open failed: %w",
18            expectedVersion, err)
19    }
20
21    // Lookup version info from plugin
22    versionSym, err := p.Lookup("GoVersion")
23    if err != nil {
24        return fmt.Errorf("plugin missing version info: %w", err)
25    }
26
27    pluginVersion := versionSym.(*string)
28    if *pluginVersion != expectedVersion {
29        return fmt.Errorf("version mismatch: plugin=%s, app=%s",
30            *pluginVersion, expectedVersion)
31    }
32
33    log.Printf("Plugin loaded successfully", *pluginVersion)
34    return nil
35}

3. No Plugin Unloading

Real-world Problem: Once you open a program, you can't uninstall it while it's running - you have to close it first. Go plugins work the same way.

Once loaded, plugins cannot be unloaded during the application's lifetime.

 1package main
 2
 3import "plugin"
 4
 5func demonstrateNoUnloading() {
 6    p, _ := plugin.Open("example.so")
 7
 8    // There is NO way to unload this plugin
 9    // It will remain in memory until the process exits
10
11    // This is a limitation of how dynamic linking works
12    _ = p
13}

Impact: Memory leaks in long-running applications. If you load 100 plugins and only need 10, the other 90 stay in memory forever.

4. Symbol Lookup Type Safety

Real-world Example: Power Adapters
You plug your device into a power adapter, but if the voltage is wrong, you might fry your device. Go plugin type assertions are similar - wrong types cause panics.

Type assertions can fail at runtime if plugin interface changes.

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "plugin"
 7)
 8
 9type PluginV1 interface {
10    Process(string) string
11}
12
13type PluginV2 interface {
14    Process(string) // Added error return
15}
16
17func loadPluginWithVersionCheck(path string) error {
18    p, err := plugin.Open(path)
19    if err != nil {
20        return err
21    }
22
23    sym, err := p.Lookup("Plugin")
24    if err != nil {
25        return err
26    }
27
28    // Try to assert to V2 first
29    if pluginV2, ok := sym.(PluginV2); ok {
30        result, err := pluginV2.Process("test")
31        if err != nil {
32            return err
33        }
34        fmt.Println("V2 plugin:", result)
35        return nil
36    }
37
38    // Fallback to V1
39    if pluginV1, ok := sym.(PluginV1); ok {
40        result := pluginV1.Process("test")
41        fmt.Println("V1 plugin:", result)
42        return nil
43    }
44
45    return fmt.Errorf("unknown plugin version")
46}

💡 Key Takeaway: These limitations are why HashiCorp built their own plugin system. Native Go plugins are great for simple use cases, but for production systems, consider RPC-based alternatives.

When to Avoid Native Plugins:

  • ❌ Cross-platform applications
  • ❌ Systems that need plugin unloading
  • ❌ Third-party plugin ecosystems
  • ❌ Long-running services with memory constraints

RPC-Based Plugin Systems

RPC plugins communicate over network protocols, providing better isolation and cross-platform support.

Basic RPC Plugin

Shared Interface:

 1// shared/interface.go
 2package shared
 3
 4type ProcessRequest struct {
 5    Data string
 6}
 7
 8type ProcessResponse struct {
 9    Result string
10    Error  string
11}

Plugin:

 1package main
 2
 3import (
 4    "log"
 5    "net"
 6    "net/rpc"
 7    "yourapp/shared"
 8)
 9
10type PluginService struct{}
11
12func Process(req *shared.ProcessRequest, resp *shared.ProcessResponse) error {
13    // Process the data
14    resp.Result = "Processed: " + req.Data
15    return nil
16}
17
18func main() {
19    service := new(PluginService)
20    rpc.Register(service)
21
22    listener, err := net.Listen("tcp", ":9999")
23    if err != nil {
24        log.Fatal(err)
25    }
26
27    log.Println("Plugin listening on :9999")
28
29    for {
30        conn, err := listener.Accept()
31        if err != nil {
32            continue
33        }
34        go rpc.ServeConn(conn)
35    }
36}

Main Application:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "net/rpc"
 7    "yourapp/shared"
 8)
 9
10type RPCPlugin struct {
11    client *rpc.Client
12}
13
14func NewRPCPlugin(address string) {
15    client, err := rpc.Dial("tcp", address)
16    if err != nil {
17        return nil, err
18    }
19
20    return &RPCPlugin{client: client}, nil
21}
22
23func Process(data string) {
24    req := &shared.ProcessRequest{Data: data}
25    resp := &shared.ProcessResponse{}
26
27    err := p.client.Call("PluginService.Process", req, resp)
28    if err != nil {
29        return "", err
30    }
31
32    if resp.Error != "" {
33        return "", fmt.Errorf(resp.Error)
34    }
35
36    return resp.Result, nil
37}
38
39func Close() error {
40    return p.client.Close()
41}
42
43func main() {
44    plugin, err := NewRPCPlugin("localhost:9999")
45    if err != nil {
46        log.Fatal(err)
47    }
48    defer plugin.Close()
49
50    result, err := plugin.Process("hello")
51    if err != nil {
52        log.Fatal(err)
53    }
54
55    fmt.Println(result)
56}

HashiCorp go-plugin Pattern

HashiCorp's go-plugin library provides a robust RPC-based plugin system with automatic process lifecycle management.

What's Happening: Instead of loading plugins as shared libraries, go-plugin runs each plugin as a separate process and communicates via RPC. This provides process isolation—if a plugin crashes, it doesn't take down the host application. The library handles all the complexity of starting processes, establishing communication, and protocol versioning.

Basic go-plugin Setup

 1// First install: go get github.com/hashicorp/go-plugin
 2
 3package main
 4
 5import (
 6    "context"
 7    "log"
 8    "os/exec"
 9
10    "github.com/hashicorp/go-plugin"
11)
12
13// Shared interface between plugin and host
14type Greeter interface {
15    Greet(name string) string
16}
17
18// RPC implementation details
19type GreeterRPC struct {
20    client *plugin.RPCClient
21}
22
23func Greet(name string) string {
24    var resp string
25    err := g.client.Call("Plugin.Greet", name, &resp)
26    if err != nil {
27        return ""
28    }
29    return resp
30}
31
32// Plugin implementation
33type GreeterPlugin struct {
34    Impl Greeter
35}
36
37func Server(*plugin.MuxBroker) {
38    return &GreeterRPCServer{Impl: p.Impl}, nil
39}
40
41func Client(b *plugin.MuxBroker, c *plugin.RPCClient) {
42    return &GreeterRPC{client: c}, nil
43}
44
45type GreeterRPCServer struct {
46    Impl Greeter
47}
48
49func Greet(name string, resp *string) error {
50    *resp = s.Impl.Greet(name)
51    return nil
52}
53
54// Host application
55func main() {
56    // Create an hclog.Logger
57    logger := plugin.NewLogger(&plugin.LoggerOptions{
58        Level: plugin.LevelTrace,
59    })
60
61    // Define plugin map
62    pluginMap := map[string]plugin.Plugin{
63        "greeter": &GreeterPlugin{},
64    }
65
66    // Launch the plugin process
67    client := plugin.NewClient(&plugin.ClientConfig{
68        HandshakeConfig: plugin.HandshakeConfig{
69            ProtocolVersion:  1,
70            MagicCookieKey:   "GREETER_PLUGIN",
71            MagicCookieValue: "hello",
72        },
73        Plugins: pluginMap,
74        Cmd:     exec.Command("./greeter-plugin"),
75        Logger:  logger,
76    })
77    defer client.Kill()
78
79    // Connect via RPC
80    rpcClient, err := client.Client()
81    if err != nil {
82        log.Fatal(err)
83    }
84
85    // Request the plugin
86    raw, err := rpcClient.Dispense("greeter")
87    if err != nil {
88        log.Fatal(err)
89    }
90
91    // Use the plugin
92    greeter := raw.(Greeter)
93    result := greeter.Greet("World")
94    log.Println(result)
95}

Plugin Process Lifecycle

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "log"
 7    "os/exec"
 8    "time"
 9
10    "github.com/hashicorp/go-plugin"
11)
12
13type PluginManager struct {
14    clients map[string]*plugin.Client
15}
16
17func NewPluginManager() *PluginManager {
18    return &PluginManager{
19        clients: make(map[string]*plugin.Client),
20    }
21}
22
23func LoadPlugin(name, path string, pluginMap map[string]plugin.Plugin) error {
24    client := plugin.NewClient(&plugin.ClientConfig{
25        HandshakeConfig: plugin.HandshakeConfig{
26            ProtocolVersion:  1,
27            MagicCookieKey:   "PLUGIN",
28            MagicCookieValue: "plugin",
29        },
30        Plugins: pluginMap,
31        Cmd:     exec.Command(path),
32    })
33
34    // Test the connection
35    rpcClient, err := client.Client()
36    if err != nil {
37        client.Kill()
38        return fmt.Errorf("connection failed: %w", err)
39    }
40
41    // Verify plugin responds
42    if err := rpcClient.Ping(); err != nil {
43        client.Kill()
44        return fmt.Errorf("ping failed: %w", err)
45    }
46
47    m.clients[name] = client
48    log.Printf("Plugin %s loaded successfully", name)
49    return nil
50}
51
52func UnloadPlugin(name string) error {
53    client, ok := m.clients[name]
54    if !ok {
55        return fmt.Errorf("plugin %s not loaded", name)
56    }
57
58    client.Kill()
59    delete(m.clients, name)
60    log.Printf("Plugin %s unloaded", name)
61    return nil
62}
63
64func RestartPlugin(name, path string, pluginMap map[string]plugin.Plugin) error {
65    if err := m.UnloadPlugin(name); err != nil {
66        return err
67    }
68
69    time.Sleep(100 * time.Millisecond) // Brief delay
70
71    return m.LoadPlugin(name, path, pluginMap)
72}
73
74func Shutdown(ctx context.Context) error {
75    done := make(chan struct{})
76
77    go func() {
78        for name := range m.clients {
79            m.UnloadPlugin(name)
80        }
81        close(done)
82    }()
83
84    select {
85    case <-done:
86        return nil
87    case <-ctx.Done():
88        return ctx.Err()
89    }
90}

Hot-Reloading Strategies

Hot-reloading allows updating plugins without restarting the main application.

File Watcher for Plugin Updates

  1package main
  2
  3import (
  4    "context"
  5    "fmt"
  6    "log"
  7    "path/filepath"
  8    "sync"
  9    "time"
 10)
 11
 12type HotReloadManager struct {
 13    mu          sync.RWMutex
 14    pluginPaths map[string]string
 15    modTimes    map[string]time.Time
 16    reloadFunc  func(string) error
 17    stopChan    chan struct{}
 18}
 19
 20func NewHotReloadManager(reloadFunc func(string) error) *HotReloadManager {
 21    return &HotReloadManager{
 22        pluginPaths: make(map[string]string),
 23        modTimes:    make(map[string]time.Time),
 24        reloadFunc:  reloadFunc,
 25        stopChan:    make(chan struct{}),
 26    }
 27}
 28
 29func Watch(name, path string) {
 30    m.mu.Lock()
 31    m.pluginPaths[name] = path
 32    m.mu.Unlock()
 33}
 34
 35func Start(ctx context.Context, interval time.Duration) {
 36    ticker := time.NewTicker(interval)
 37    defer ticker.Stop()
 38
 39    for {
 40        select {
 41        case <-ticker.C:
 42            m.checkForUpdates()
 43        case <-ctx.Done():
 44            return
 45        case <-m.stopChan:
 46            return
 47        }
 48    }
 49}
 50
 51func checkForUpdates() {
 52    m.mu.RLock()
 53    paths := make(map[string]string)
 54    for k, v := range m.pluginPaths {
 55        paths[k] = v
 56    }
 57    m.mu.RUnlock()
 58
 59    for name, path := range paths {
 60        info, err := filepath.Glob(path)
 61        if err != nil || len(info) == 0 {
 62            continue
 63        }
 64
 65        // Get file modification time
 66        stat, err := filepath.Stat(info[0])
 67        if err != nil {
 68            continue
 69        }
 70
 71        modTime := stat.ModTime()
 72
 73        m.mu.Lock()
 74        lastModTime, exists := m.modTimes[name]
 75        m.mu.Unlock()
 76
 77        if !exists || modTime.After(lastModTime) {
 78            log.Printf("Plugin %s changed, reloading...", name)
 79
 80            if err := m.reloadFunc(name); err != nil {
 81                log.Printf("Failed to reload plugin %s: %v", name, err)
 82                continue
 83            }
 84
 85            m.mu.Lock()
 86            m.modTimes[name] = modTime
 87            m.mu.Unlock()
 88
 89            log.Printf("Plugin %s reloaded successfully", name)
 90        }
 91    }
 92}
 93
 94func Stop() {
 95    close(m.stopChan)
 96}
 97
 98// Usage example
 99func main() {
100    reloadFunc := func(name string) error {
101        log.Printf("Reloading plugin: %s", name)
102        // Implement actual reload logic here
103        return nil
104    }
105
106    manager := NewHotReloadManager(reloadFunc)
107    manager.Watch("my-plugin", "./plugins/*.so")
108
109    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
110    defer cancel()
111
112    go manager.Start(ctx, 1*time.Second)
113
114    // Run application...
115    time.Sleep(5 * time.Minute)
116    manager.Stop()
117}

Version-Based Hot Reload

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "sync"
 7    "sync/atomic"
 8)
 9
10type VersionedPlugin struct {
11    version uint64
12    plugin  interface{}
13}
14
15type VersionedPluginManager struct {
16    mu      sync.RWMutex
17    plugins map[string]*VersionedPlugin
18}
19
20func NewVersionedPluginManager() *VersionedPluginManager {
21    return &VersionedPluginManager{
22        plugins: make(map[string]*VersionedPlugin),
23    }
24}
25
26func Load(name string, plugin interface{}) {
27    m.mu.Lock()
28    defer m.mu.Unlock()
29
30    vp, exists := m.plugins[name]
31    if !exists {
32        vp = &VersionedPlugin{
33            version: 0,
34            plugin:  plugin,
35        }
36        m.plugins[name] = vp
37    } else {
38        // Atomically increment version
39        atomic.AddUint64(&vp.version, 1)
40        vp.plugin = plugin
41    }
42
43    log.Printf("Loaded plugin %s version %d", name, vp.version)
44}
45
46func Get(name string) {
47    m.mu.RLock()
48    defer m.mu.RUnlock()
49
50    vp, exists := m.plugins[name]
51    if !exists {
52        return nil, 0, false
53    }
54
55    version := atomic.LoadUint64(&vp.version)
56    return vp.plugin, version, true
57}
58
59func Reload(name string, newPlugin interface{}) error {
60    m.mu.Lock()
61    defer m.mu.Unlock()
62
63    vp, exists := m.plugins[name]
64    if !exists {
65        return fmt.Errorf("plugin %s not found", name)
66    }
67
68    oldVersion := atomic.LoadUint64(&vp.version)
69    atomic.AddUint64(&vp.version, 1)
70    newVersion := atomic.LoadUint64(&vp.version)
71
72    vp.plugin = newPlugin
73
74    log.Printf("Reloaded plugin %s: v%d -> v%d", name, oldVersion, newVersion)
75    return nil
76}

Sandboxing and Security

When loading plugins, especially from untrusted sources, security is critical.

Resource Limits with RPC Plugins

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "log"
 7    "os/exec"
 8    "syscall"
 9    "time"
10)
11
12type SecurePluginConfig struct {
13    MaxMemoryMB  int
14    MaxCPUTime   time.Duration
15    Timeout      time.Duration
16    RestrictNet  bool
17    RestrictDisk bool
18}
19
20type SecurePluginRunner struct {
21    config SecurePluginConfig
22}
23
24func NewSecurePluginRunner(config SecurePluginConfig) *SecurePluginRunner {
25    return &SecurePluginRunner{config: config}
26}
27
28func RunPlugin(ctx context.Context, pluginPath string, args []string) error {
29    // Create context with timeout
30    ctx, cancel := context.WithTimeout(ctx, r.config.Timeout)
31    defer cancel()
32
33    // Create command
34    cmd := exec.CommandContext(ctx, pluginPath, args...)
35
36    // Set resource limits
37    cmd.SysProcAttr = &syscall.SysProcAttr{
38        // Set memory limit
39        // This is platform-specific
40    }
41
42    // Start the plugin process
43    if err := cmd.Start(); err != nil {
44        return fmt.Errorf("failed to start plugin: %w", err)
45    }
46
47    // Monitor the process
48    done := make(chan error, 1)
49    go func() {
50        done <- cmd.Wait()
51    }()
52
53    select {
54    case <-ctx.Done():
55        // Timeout or cancellation
56        if err := cmd.Process.Kill(); err != nil {
57            log.Printf("Failed to kill plugin process: %v", err)
58        }
59        return fmt.Errorf("plugin execution timeout")
60
61    case err := <-done:
62        if err != nil {
63            return fmt.Errorf("plugin execution failed: %w", err)
64        }
65        return nil
66    }
67}

Input Validation and Sanitization

 1package main
 2
 3import (
 4    "fmt"
 5    "regexp"
 6    "strings"
 7)
 8
 9type PluginValidator struct {
10    maxInputSize int
11    allowedChars *regexp.Regexp
12}
13
14func NewPluginValidator() *PluginValidator {
15    return &PluginValidator{
16        maxInputSize: 1024 * 1024, // 1MB
17        allowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.,!?]+$`),
18    }
19}
20
21func ValidateInput(input string) error {
22    // Check size
23    if len(input) > v.maxInputSize {
24        return fmt.Errorf("input too large: %d bytes",
25            len(input), v.maxInputSize)
26    }
27
28    // Check for null bytes
29    if strings.Contains(input, "\x00") {
30        return fmt.Errorf("input contains null bytes")
31    }
32
33    // Check allowed characters
34    if !v.allowedChars.MatchString(input) {
35        return fmt.Errorf("input contains disallowed characters")
36    }
37
38    return nil
39}
40
41func SanitizeInput(input string) string {
42    // Remove control characters
43    sanitized := strings.Map(func(r rune) rune {
44        if r < 32 || r == 127 {
45            return -1 // Remove character
46        }
47        return r
48    }, input)
49
50    // Trim to max size
51    if len(sanitized) > v.maxInputSize {
52        sanitized = sanitized[:v.maxInputSize]
53    }
54
55    return sanitized
56}
57
58// Usage in plugin call
59func CallPluginSafely(plugin interface{}, input string) {
60    validator := NewPluginValidator()
61
62    // Validate input
63    if err := validator.ValidateInput(input); err != nil {
64        return "", fmt.Errorf("invalid input: %w", err)
65    }
66
67    // Sanitize as additional safety
68    sanitized := validator.SanitizeInput(input)
69
70    // Call plugin with sanitized input
71    //
72    result := fmt.Sprintf("Processed: %s", sanitized)
73
74    return result, nil
75}

WebAssembly Plugins

WebAssembly provides strong sandboxing for untrusted plugins.

Basic Wasm Plugin

 1// go get github.com/wasmerio/wasmer-go/wasmer
 2
 3package main
 4
 5import (
 6    "fmt"
 7    "log"
 8
 9    wasmer "github.com/wasmerio/wasmer-go/wasmer"
10)
11
12func main() {
13    // Read the Wasm module
14    wasmBytes, err := wasmer.ReadBytes("plugin.wasm")
15    if err != nil {
16        log.Fatal(err)
17    }
18
19    // Create store
20    engine := wasmer.NewEngine()
21    store := wasmer.NewStore(engine)
22
23    // Compile the module
24    module, err := wasmer.NewModule(store, wasmBytes)
25    if err != nil {
26        log.Fatal(err)
27    }
28
29    // Instantiate the module
30    importObject := wasmer.NewImportObject()
31    instance, err := wasmer.NewInstance(module, importObject)
32    if err != nil {
33        log.Fatal(err)
34    }
35
36    // Get exported function
37    add, err := instance.Exports.GetFunction("add")
38    if err != nil {
39        log.Fatal(err)
40    }
41
42    // Call the function
43    result, err := add(5, 7)
44    if err != nil {
45        log.Fatal(err)
46    }
47
48    fmt.Printf("Result: %v\n", result)
49}

Best Practices and Alternatives

When to Use Native Plugins

  • ✅ Trust the plugin developers
  • ✅ Need maximum performance
  • ✅ Same-language ecosystem
  • ❌ Limited to Linux/macOS/FreeBSD
  • ❌ Strict version matching required

When to Use RPC Plugins

  • ✅ Need cross-platform support
  • ✅ Want process isolation
  • ✅ Need to unload/reload plugins
  • ✅ Untrusted plugin code
  • ❌ Slight performance overhead

When to Use WebAssembly

  • ✅ Maximum security/sandboxing needed
  • ✅ Cross-platform requirement
  • ✅ Untrusted third-party plugins
  • ❌ Limited API surface
  • ❌ Some performance overhead

Alternatives to Plugins

1. Embedded Scripting:

1// Using github.com/yuin/gopher-lua
2import lua "github.com/yuin/gopher-lua"
3
4func runLuaPlugin(script string) error {
5    L := lua.NewState()
6    defer L.Close()
7
8    return L.DoString(script)
9}

2. Configuration-Based Extensions:

 1// Simple configuration-driven behavior
 2type Action struct {
 3    Type   string            `json:"type"`
 4    Config map[string]string `json:"config"`
 5}
 6
 7func executeAction(action Action) error {
 8    switch action.Type {
 9    case "http":
10        // HTTP request action
11    case "email":
12        // Email action
13    default:
14        return fmt.Errorf("unknown action: %s", action.Type)
15    }
16    return nil
17}

3. Microservices Architecture:

 1// Instead of plugins, use independent services
 2type ServiceRegistry struct {
 3    services map[string]string // name -> URL
 4}
 5
 6func CallService(name, method string, args interface{}) {
 7    url := r.services[name]
 8    // Make HTTP/gRPC call to service
 9    return nil, nil
10}

Real-world Plugin Examples to Study

  1. Docker Plugin System - Storage, network, and authorization plugins
  2. Kubernetes Operators - Custom controllers via CRDs
  3. VS Code Extensions - Language servers, themes, debuggers
  4. WordPress Plugins - PHP-based plugin ecosystem
  5. Chrome Extensions - Web extensions with security sandboxes

Each of these systems solved real-world extensibility challenges. Study their approaches to plugin interfaces, security models, and developer experience.

Lua Scripting Integration

Embed Lua as a lightweight scripting engine for plugins using gopher-lua.

  1package main
  2
  3import (
  4    "fmt"
  5    "log"
  6
  7    lua "github.com/yuin/gopher-lua"
  8)
  9
 10// LuaPlugin wraps Lua script execution
 11type LuaPlugin struct {
 12    state *lua.LState
 13    name  string
 14}
 15
 16func NewLuaPlugin(name, script string) {
 17    L := lua.NewState()
 18
 19    // Load and compile script
 20    if err := L.DoString(script); err != nil {
 21        L.Close()
 22        return nil, fmt.Errorf("failed to load Lua script: %w", err)
 23    }
 24
 25    return &LuaPlugin{
 26        state: L,
 27        name:  name,
 28    }, nil
 29}
 30
 31func CallFunction(fnName string, args ...interface{}) {
 32    L := p.state
 33
 34    // Get function from Lua
 35    fn := L.GetGlobal(fnName)
 36    if fn.Type() != lua.LTFunction {
 37        return nil, fmt.Errorf("function %s not found", fnName)
 38    }
 39
 40    // Convert Go args to Lua values
 41    luaArgs := make([]lua.LValue, len(args))
 42    for i, arg := range args {
 43        luaArgs[i] = goToLua(L, arg)
 44    }
 45
 46    // Call function
 47    if err := L.CallByParam(lua.P{
 48        Fn:      fn,
 49        NRet:    1,
 50        Protect: true,
 51    }, luaArgs...); err != nil {
 52        return nil, err
 53    }
 54
 55    // Get return value
 56    ret := L.Get(-1)
 57    L.Pop(1)
 58
 59    return luaToGo(ret), nil
 60}
 61
 62func Close() error {
 63    p.state.Close()
 64    return nil
 65}
 66
 67// Helper functions to convert between Go and Lua values
 68func goToLua(L *lua.LState, value interface{}) lua.LValue {
 69    switch v := value.(type) {
 70    case string:
 71        return lua.LString(v)
 72    case int:
 73        return lua.LNumber(v)
 74    case float64:
 75        return lua.LNumber(v)
 76    case bool:
 77        return lua.LBool(v)
 78    default:
 79        return lua.LNil
 80    }
 81}
 82
 83func luaToGo(value lua.LValue) interface{} {
 84    switch v := value.(type) {
 85    case lua.LString:
 86        return string(v)
 87    case lua.LNumber:
 88        return float64(v)
 89    case lua.LBool:
 90        return bool(v)
 91    default:
 92        return nil
 93    }
 94}
 95
 96// Example: Data transformation plugin
 97func main() {
 98    script := `
 99        function transform(data)
100            -- Transform data in Lua
101            return string.upper(data) .. " - processed by Lua"
102        end
103
104        function validate(input)
105            if string.len(input) < 5 then
106                return false
107            end
108            return true
109        end
110    `
111
112    plugin, err := NewLuaPlugin("transformer", script)
113    if err != nil {
114        log.Fatal(err)
115    }
116    defer plugin.Close()
117
118    // Call Lua functions from Go
119    result, err := plugin.CallFunction("transform", "hello world")
120    if err != nil {
121        log.Fatal(err)
122    }
123    fmt.Printf("Transform result: %v\n", result)
124
125    valid, err := plugin.CallFunction("validate", "test")
126    if err != nil {
127        log.Fatal(err)
128    }
129    fmt.Printf("Validation result: %v\n", valid)
130}

Lua Plugin with Go API Exposure

Expose Go functions to Lua scripts:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6
 7    lua "github.com/yuin/gopher-lua"
 8)
 9
10type APIService struct {
11    database map[string]string
12}
13
14func NewAPIService() *APIService {
15    return &APIService{
16        database: make(map[string]string),
17    }
18}
19
20func Get(key string) string {
21    return s.database[key]
22}
23
24func Set(key, value string) {
25    s.database[key] = value
26}
27
28// RegisterAPI exposes Go functions to Lua
29func RegisterAPI(L *lua.LState, service *APIService) {
30    // Create API table
31    apiTable := L.NewTable()
32
33    // Register get function
34    L.SetField(apiTable, "get", L.NewFunction(func(L *lua.LState) int {
35        key := L.CheckString(1)
36        value := service.Get(key)
37        L.Push(lua.LString(value))
38        return 1
39    }))
40
41    // Register set function
42    L.SetField(apiTable, "set", L.NewFunction(func(L *lua.LState) int {
43        key := L.CheckString(1)
44        value := L.CheckString(2)
45        service.Set(key, value)
46        return 0
47    }))
48
49    // Make API available as global
50    L.SetGlobal("api", apiTable)
51}
52
53func main() {
54    L := lua.NewState()
55    defer L.Close()
56
57    service := NewAPIService()
58    RegisterAPI(L, service)
59
60    script := `
61        -- Lua script can now call Go functions
62        api.set("user:1", "John Doe")
63        api.set("user:2", "Jane Smith")
64
65        name = api.get("user:1")
66        print("Retrieved: " .. name)
67
68        function getUser(id)
69            return api.get("user:" .. id)
70        end
71    `
72
73    if err := L.DoString(script); err != nil {
74        log.Fatal(err)
75    }
76
77    // Call Lua function that uses Go API
78    if err := L.CallByParam(lua.P{
79        Fn:      L.GetGlobal("getUser"),
80        NRet:    1,
81        Protect: true,
82    }, lua.LNumber(2)); err != nil {
83        log.Fatal(err)
84    }
85
86    result := L.Get(-1)
87    L.Pop(1)
88
89    fmt.Printf("Result from Lua: %s\n", result.String())
90}

WebAssembly Plugins

Run untrusted code safely with WebAssembly:

  1package main
  2
  3import (
  4    "context"
  5    "fmt"
  6    "log"
  7    "os"
  8
  9    "github.com/tetratelabs/wazero"
 10    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
 11)
 12
 13type WasmPlugin struct {
 14    runtime wazero.Runtime
 15    module  wazero.CompiledModule
 16    name    string
 17}
 18
 19func NewWasmPlugin(name string, wasmPath string) {
 20    ctx := context.Background()
 21
 22    // Create runtime
 23    r := wazero.NewRuntime(ctx)
 24
 25    // Instantiate WASI
 26    wasi_snapshot_preview1.MustInstantiate(ctx, r)
 27
 28    // Read WASM file
 29    wasmBytes, err := os.ReadFile(wasmPath)
 30    if err != nil {
 31        r.Close(ctx)
 32        return nil, err
 33    }
 34
 35    // Compile module
 36    compiled, err := r.CompileModule(ctx, wasmBytes)
 37    if err != nil {
 38        r.Close(ctx)
 39        return nil, err
 40    }
 41
 42    return &WasmPlugin{
 43        runtime: r,
 44        module:  compiled,
 45        name:    name,
 46    }, nil
 47}
 48
 49func Execute(functionName string, args ...uint64) {
 50    ctx := context.Background()
 51
 52    // Instantiate module
 53    instance, err := p.runtime.InstantiateModule(ctx, p.module, wazero.NewModuleConfig())
 54    if err != nil {
 55        return nil, err
 56    }
 57    defer instance.Close(ctx)
 58
 59    // Get exported function
 60    fn := instance.ExportedFunction(functionName)
 61    if fn == nil {
 62        return nil, fmt.Errorf("function %s not found", functionName)
 63    }
 64
 65    // Call function
 66    results, err := fn.Call(ctx, args...)
 67    if err != nil {
 68        return nil, err
 69    }
 70
 71    return results, nil
 72}
 73
 74func Close() error {
 75    return p.runtime.Close(context.Background())
 76}
 77
 78// Example: Image filter plugin in WASM
 79func ExampleImageFilter() {
 80    plugin, err := NewWasmPlugin("image-filter", "filter.wasm")
 81    if err != nil {
 82        log.Fatal(err)
 83    }
 84    defer plugin.Close()
 85
 86    // Call WASM function
 87    // filter.wasm exports: brightness(imageDataPtr, length, factor) -> resultPtr
 88    results, err := plugin.Execute("brightness", 0, 1024, 120)
 89    if err != nil {
 90        log.Fatal(err)
 91    }
 92
 93    fmt.Printf("Filter applied, result pointer: %v\n", results)
 94}
 95
 96// WASM Plugin with Memory Sharing
 97type WasmPluginWithMemory struct {
 98    *WasmPlugin
 99    memory wazero.Memory
100}
101
102func NewWasmPluginWithMemory(name, wasmPath string) {
103    basePlugin, err := NewWasmPlugin(name, wasmPath)
104    if err != nil {
105        return nil, err
106    }
107
108    ctx := context.Background()
109
110    // Instantiate to get memory
111    instance, err := basePlugin.runtime.InstantiateModule(ctx, basePlugin.module,
112        wazero.NewModuleConfig())
113    if err != nil {
114        basePlugin.Close()
115        return nil, err
116    }
117
118    memory := instance.Memory()
119
120    return &WasmPluginWithMemory{
121        WasmPlugin: basePlugin,
122        memory:     memory,
123    }, nil
124}
125
126func WriteMemory(offset uint32, data []byte) error {
127    return p.memory.Write(offset, data)
128}
129
130func ReadMemory(offset uint32, length uint32) {
131    data, ok := p.memory.Read(offset, length)
132    if !ok {
133        return nil, fmt.Errorf("failed to read memory at offset %d", offset)
134    }
135    return data, nil
136}
137
138// Production WASM Plugin Manager
139type WasmPluginManager struct {
140    plugins map[string]*WasmPlugin
141}
142
143func NewWasmPluginManager() *WasmPluginManager {
144    return &WasmPluginManager{
145        plugins: make(map[string]*WasmPlugin),
146    }
147}
148
149func Load(name, path string) error {
150    plugin, err := NewWasmPlugin(name, path)
151    if err != nil {
152        return err
153    }
154
155    m.plugins[name] = plugin
156    return nil
157}
158
159func Execute(name, function string, args ...uint64) {
160    plugin, ok := m.plugins[name]
161    if !ok {
162        return nil, fmt.Errorf("plugin %s not found", name)
163    }
164
165    return plugin.Execute(function, args...)
166}
167
168func Unload(name string) error {
169    plugin, ok := m.plugins[name]
170    if !ok {
171        return fmt.Errorf("plugin %s not found", name)
172    }
173
174    delete(m.plugins, name)
175    return plugin.Close()
176}
177
178func Shutdown() error {
179    for _, plugin := range m.plugins {
180        if err := plugin.Close(); err != nil {
181            return err
182        }
183    }
184    m.plugins = make(map[string]*WasmPlugin)
185    return nil
186}
187
188func main() {
189    manager := NewWasmPluginManager()
190    defer manager.Shutdown()
191
192    // Load plugins
193    if err := manager.Load("filter", "filter.wasm"); err != nil {
194        log.Fatal(err)
195    }
196
197    if err := manager.Load("processor", "processor.wasm"); err != nil {
198        log.Fatal(err)
199    }
200
201    // Execute plugin functions
202    result, err := manager.Execute("filter", "apply", 100, 200)
203    if err != nil {
204        log.Fatal(err)
205    }
206
207    fmt.Printf("Plugin result: %v\n", result)
208}

WASM Plugin Example

Example Rust code compiled to WASM for use as plugin:

 1// plugin.rs - Compile with: rustc --target wasm32-wasi plugin.rs -o plugin.wasm
 2
 3#[no_mangle]
 4pub extern "C" fn add(a: i32, b: i32) -> i32 {
 5    a + b
 6}
 7
 8#[no_mangle]
 9pub extern "C" fn process_string(ptr: *const u8, len: usize) -> i32 {
10    // Read string from memory
11    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
12    let s = std::str::from_utf8(slice).unwrap();
13
14    // Process string
15    s.split_whitespace().count() as i32
16}
17
18#[no_mangle]
19pub extern "C" fn transform(value: i32) -> i32 {
20    // Example transformation
21    value * 2 + 10
22}

Advanced Plugin Patterns

Plugin Registry and Discovery

Production plugin systems need sophisticated discovery and management. Here's a complete registry pattern:

  1package plugin_registry
  2
  3import (
  4	"fmt"
  5	"sync"
  6)
  7
  8// PluginInfo contains metadata about a plugin
  9type PluginInfo struct {
 10	Name        string
 11	Version     string
 12	Author      string
 13	Description string
 14	Plugin      interface{}
 15}
 16
 17// PluginRegistry manages all loaded plugins
 18type PluginRegistry struct {
 19	mu      sync.RWMutex
 20	plugins map[string]*PluginInfo
 21	hooks   map[string][]func(interface{}) error
 22}
 23
 24// NewPluginRegistry creates a new registry
 25func NewPluginRegistry() *PluginRegistry {
 26	return &PluginRegistry{
 27		plugins: make(map[string]*PluginInfo),
 28		hooks:   make(map[string][]func(interface{}) error),
 29	}
 30}
 31
 32// Register adds a plugin to the registry
 33func (r *PluginRegistry) Register(info *PluginInfo) error {
 34	r.mu.Lock()
 35	defer r.mu.Unlock()
 36
 37	if _, exists := r.plugins[info.Name]; exists {
 38		return fmt.Errorf("plugin %s already registered", info.Name)
 39	}
 40
 41	r.plugins[info.Name] = info
 42	r.triggerHooks("plugin:registered", info)
 43	return nil
 44}
 45
 46// Get retrieves a plugin by name
 47func (r *PluginRegistry) Get(name string) (*PluginInfo, bool) {
 48	r.mu.RLock()
 49	defer r.mu.RUnlock()
 50	plugin, exists := r.plugins[name]
 51	return plugin, exists
 52}
 53
 54// List returns all registered plugins
 55func (r *PluginRegistry) List() []*PluginInfo {
 56	r.mu.RLock()
 57	defer r.mu.RUnlock()
 58
 59	result := make([]*PluginInfo, 0, len(r.plugins))
 60	for _, plugin := range r.plugins {
 61		result = append(result, plugin)
 62	}
 63	return result
 64}
 65
 66// Unregister removes a plugin
 67func (r *PluginRegistry) Unregister(name string) error {
 68	r.mu.Lock()
 69	defer r.mu.Unlock()
 70
 71	if _, exists := r.plugins[name]; !exists {
 72		return fmt.Errorf("plugin %s not found", name)
 73	}
 74
 75	delete(r.plugins, name)
 76	r.triggerHooks("plugin:unregistered", name)
 77	return nil
 78}
 79
 80// RegisterHook registers a callback for lifecycle events
 81func (r *PluginRegistry) RegisterHook(event string, fn func(interface{}) error) {
 82	r.mu.Lock()
 83	defer r.mu.Unlock()
 84	r.hooks[event] = append(r.hooks[event], fn)
 85}
 86
 87// triggerHooks executes all registered hooks for an event
 88func (r *PluginRegistry) triggerHooks(event string, data interface{}) {
 89	hooks, exists := r.hooks[event]
 90	if !exists {
 91		return
 92	}
 93
 94	for _, hook := range hooks {
 95		_ = hook(data)
 96	}
 97}
 98
 99// Example usage
100type APIServer interface {
101	Start() error
102	Stop() error
103	ServeHTTP(w interface{}, r interface{})
104}
105
106func main() {
107	registry := NewPluginRegistry()
108
109	// Register plugins
110	registry.Register(&PluginInfo{
111		Name:        "api_server",
112		Version:     "1.0.0",
113		Author:      "admin",
114		Description: "REST API server plugin",
115		Plugin:      nil,
116	})
117
118	// List all plugins
119	for _, p := range registry.List() {
120		fmt.Printf("Loaded: %s v%s - %s\n", p.Name, p.Version, p.Description)
121	}
122}

Plugin Versioning and Compatibility

Managing plugin versions and ensuring compatibility is crucial for production systems:

 1package plugin_versioning
 2
 3import "fmt"
 4
 5// Version represents a semantic version
 6type Version struct {
 7	Major, Minor, Patch int
 8}
 9
10// PluginVersion stores version requirements
11type PluginVersion struct {
12	Current     Version
13	MinRequired Version
14	MaxAllowed  Version
15}
16
17// IsCompatible checks if host version is compatible
18func (pv *PluginVersion) IsCompatible(hostVersion Version) bool {
19	// Check if host >= min and host <= max
20	if !hostVersion.GreaterOrEqual(pv.MinRequired) {
21		return false
22	}
23	if !hostVersion.LessOrEqual(pv.MaxAllowed) {
24		return false
25	}
26	return true
27}
28
29// GreaterOrEqual checks if v >= other
30func (v Version) GreaterOrEqual(other Version) bool {
31	if v.Major != other.Major {
32		return v.Major > other.Major
33	}
34	if v.Minor != other.Minor {
35		return v.Minor > other.Minor
36	}
37	return v.Patch >= other.Patch
38}
39
40// LessOrEqual checks if v <= other
41func (v Version) LessOrEqual(other Version) bool {
42	if v.Major != other.Major {
43		return v.Major < other.Major
44	}
45	if v.Minor != other.Minor {
46		return v.Minor < other.Minor
47	}
48	return v.Patch <= other.Patch
49}
50
51// Example
52func main() {
53	plugin := PluginVersion{
54		Current:     Version{1, 5, 0},
55		MinRequired: Version{1, 0, 0},
56		MaxAllowed:  Version{2, 0, 0},
57	}
58
59	hostV1 := Version{1, 3, 0}
60	hostV2 := Version{3, 0, 0}
61
62	fmt.Printf("Host 1.3.0 compatible: %v\n", plugin.IsCompatible(hostV1))
63	fmt.Printf("Host 3.0.0 compatible: %v\n", plugin.IsCompatible(hostV2))
64}

Plugin Configuration and Metadata

Plugins often need configuration. Here's a pattern for plugin configuration with validation:

 1package plugin_config
 2
 3import (
 4	"encoding/json"
 5	"fmt"
 6)
 7
 8// PluginConfig provides standardized configuration
 9type PluginConfig struct {
10	Name       string                 `json:"name"`
11	Enabled    bool                   `json:"enabled"`
12	Priority   int                    `json:"priority"`
13	Settings   map[string]interface{} `json:"settings"`
14	Validators map[string]func(interface{}) error
15}
16
17// Load loads configuration from JSON
18func (pc *PluginConfig) Load(data []byte) error {
19	if err := json.Unmarshal(data, pc); err != nil {
20		return fmt.Errorf("failed to unmarshal config: %w", err)
21	}
22	return pc.Validate()
23}
24
25// Validate checks all settings against validators
26func (pc *PluginConfig) Validate() error {
27	if pc.Name == "" {
28		return fmt.Errorf("plugin name is required")
29	}
30
31	if pc.Priority < 0 || pc.Priority > 100 {
32		return fmt.Errorf("priority must be 0-100, got %d", pc.Priority)
33	}
34
35	for key, validator := range pc.Validators {
36		if value, exists := pc.Settings[key]; exists {
37			if err := validator(value); err != nil {
38				return fmt.Errorf("validation failed for %s: %w", key, err)
39			}
40		}
41	}
42
43	return nil
44}
45
46// GetString gets a string setting with default
47func (pc *PluginConfig) GetString(key, defaultVal string) string {
48	if val, exists := pc.Settings[key]; exists {
49		if str, ok := val.(string); ok {
50			return str
51		}
52	}
53	return defaultVal
54}
55
56// GetInt gets an int setting with default
57func (pc *PluginConfig) GetInt(key string, defaultVal int) int {
58	if val, exists := pc.Settings[key]; exists {
59		if num, ok := val.(float64); ok {
60			return int(num)
61		}
62	}
63	return defaultVal
64}

Integration and Mastery - Production Plugin Systems

Building a Sustainable Plugin Ecosystem:

  1. Start Simple: Begin with interface-based plugins before moving to dynamic loading
  2. Version Carefully: Semantic versioning prevents incompatibility issues
  3. Security First: Validate, sandbox, and monitor all plugins
  4. Documentation: Clear plugin contracts make integration easier
  5. Testing: Test plugin loading, isolation, and failure scenarios
  6. Monitoring: Track plugin performance and health
  7. Community: Foster a plugin ecosystem with documentation and tools

Plugin architecture enables building platforms instead of products. When done well, plugins attract developers, create ecosystems, and enable innovation beyond what the core team could achieve alone.

Practice Exercises

Exercise 1: Basic Plugin System

Difficulty: Intermediate | Time: 60-90 minutes | Learning Objectives: Master Go's plugin package, design extensible interfaces, and build dynamic loading systems.

Create a plugin system that loads transformation plugins implementing a Transform interface for string conversions. This foundational exercise teaches you to build extensible architectures where functionality can be added without modifying core code—a pattern used by IDEs, web browsers, and build tools. You'll learn to design clean plugin interfaces, handle dynamic library loading safely, and create discovery mechanisms that automatically find and load plugins from directories. These skills are essential for building applications that need to support third-party extensions or modular functionality.

Solution
  1// shared/transformer.go
  2package shared
  3
  4type Transformer interface {
  5    Transform(string) string
  6    Name() string
  7}
  8
  9// plugins/uppercase/main.go
 10package main
 11
 12import "strings"
 13
 14type UppercaseTransformer struct{}
 15
 16func Transform(s string) string {
 17    return strings.ToUpper(s)
 18}
 19
 20func Name() string {
 21    return "uppercase"
 22}
 23
 24var Transformer UppercaseTransformer
 25
 26func main() {}
 27
 28// Build: go build -buildmode=plugin -o uppercase.so plugins/uppercase/main.go
 29
 30// plugins/reverse/main.go
 31package main
 32
 33type ReverseTransformer struct{}
 34
 35func Transform(s string) string {
 36    runes := []rune(s)
 37    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
 38        runes[i], runes[j] = runes[j], runes[i]
 39    }
 40    return string(runes)
 41}
 42
 43func Name() string {
 44    return "reverse"
 45}
 46
 47var Transformer ReverseTransformer
 48
 49func main() {}
 50
 51// main.go
 52package main
 53
 54import (
 55    "fmt"
 56    "log"
 57    "os"
 58    "path/filepath"
 59    "plugin"
 60    "yourapp/shared"
 61)
 62
 63func main() {
 64    if len(os.Args) < 3 {
 65        fmt.Println("Usage: program <plugin-dir> <text>")
 66        return
 67    }
 68
 69    dir := os.Args[1]
 70    text := os.Args[2]
 71
 72    // Load all plugins
 73    transformers := make(map[string]shared.Transformer)
 74
 75    matches, err := filepath.Glob(filepath.Join(dir, "*.so"))
 76    if err != nil {
 77        log.Fatal(err)
 78    }
 79
 80    for _, path := range matches {
 81        p, err := plugin.Open(path)
 82        if err != nil {
 83            log.Printf("Failed to load %s: %v", path, err)
 84            continue
 85        }
 86
 87        sym, err := p.Lookup("Transformer")
 88        if err != nil {
 89            log.Printf("No Transformer in %s: %v", path, err)
 90            continue
 91        }
 92
 93        transformer := sym.(shared.Transformer)
 94        transformers[transformer.Name()] = transformer
 95        fmt.Printf("Loaded plugin: %s\n", transformer.Name())
 96    }
 97
 98    // Apply all transformations
 99    for name, transformer := range transformers {
100        result := transformer.Transform(text)
101        fmt.Printf("%s: %s\n", name, result)
102    }
103}

Exercise 2: RPC Plugin with Health Checks

Difficulty: Advanced | Time: 90-120 minutes | Learning Objectives: Master RPC plugin patterns, implement process isolation, and build resilient plugin management systems.

Build an RPC-based plugin system where the host regularly checks plugin health and automatically restarts unhealthy plugins, providing robust failure isolation and recovery mechanisms. This advanced exercise teaches you to create production-ready plugin architectures that can handle plugin failures gracefully without affecting the main application—a critical pattern for microservices, content management systems, and extensible platforms. You'll learn to implement health monitoring systems, design automatic recovery mechanisms, and build communication protocols that handle network failures and timeouts. These patterns are essential for building reliable distributed systems with plugin architectures.

Solution
  1// shared/interface.go
  2package shared
  3
  4type HealthStatus struct {
  5    Healthy bool
  6    Message string
  7}
  8
  9type ProcessRequest struct {
 10    Data string
 11}
 12
 13type ProcessResponse struct {
 14    Result string
 15}
 16
 17// plugin/main.go
 18package main
 19
 20import (
 21    "log"
 22    "net"
 23    "net/rpc"
 24    "time"
 25    "yourapp/shared"
 26)
 27
 28type PluginService struct {
 29    startTime time.Time
 30    callCount int
 31}
 32
 33func NewPluginService() *PluginService {
 34    return &PluginService{
 35        startTime: time.Now(),
 36    }
 37}
 38
 39func Process(req *shared.ProcessRequest, resp *shared.ProcessResponse) error {
 40    s.callCount++
 41    resp.Result = "Processed: " + req.Data
 42    return nil
 43}
 44
 45func Health(_ struct{}, status *shared.HealthStatus) error {
 46    status.Healthy = true
 47    status.Message = fmt.Sprintf("Uptime: %v, Calls: %d",
 48        time.Since(s.startTime), s.callCount)
 49    return nil
 50}
 51
 52func main() {
 53    service := NewPluginService()
 54    rpc.Register(service)
 55
 56    listener, err := net.Listen("tcp", ":9999")
 57    if err != nil {
 58        log.Fatal(err)
 59    }
 60
 61    log.Println("Plugin listening on :9999")
 62
 63    for {
 64        conn, err := listener.Accept()
 65        if err != nil {
 66            continue
 67        }
 68        go rpc.ServeConn(conn)
 69    }
 70}
 71
 72// main.go
 73package main
 74
 75import (
 76    "context"
 77    "fmt"
 78    "log"
 79    "net/rpc"
 80    "os/exec"
 81    "sync"
 82    "time"
 83    "yourapp/shared"
 84)
 85
 86type ManagedPlugin struct {
 87    name    string
 88    address string
 89    cmd     *exec.Cmd
 90    client  *rpc.Client
 91    healthy bool
 92    mu      sync.RWMutex
 93}
 94
 95func NewManagedPlugin(name, address, binPath string) {
 96    mp := &ManagedPlugin{
 97        name:    name,
 98        address: address,
 99    }
100
101    if err := mp.Start(binPath); err != nil {
102        return nil, err
103    }
104
105    return mp, nil
106}
107
108func Start(binPath string) error {
109    mp.mu.Lock()
110    defer mp.mu.Unlock()
111
112    // Start plugin process
113    mp.cmd = exec.Command(binPath)
114    if err := mp.cmd.Start(); err != nil {
115        return err
116    }
117
118    // Wait for plugin to be ready
119    time.Sleep(500 * time.Millisecond)
120
121    // Connect to plugin
122    client, err := rpc.Dial("tcp", mp.address)
123    if err != nil {
124        mp.cmd.Process.Kill()
125        return err
126    }
127
128    mp.client = client
129    mp.healthy = true
130
131    log.Printf("Plugin %s started", mp.name)
132    return nil
133}
134
135func Stop() error {
136    mp.mu.Lock()
137    defer mp.mu.Unlock()
138
139    if mp.client != nil {
140        mp.client.Close()
141    }
142
143    if mp.cmd != nil && mp.cmd.Process != nil {
144        mp.cmd.Process.Kill()
145    }
146
147    mp.healthy = false
148    log.Printf("Plugin %s stopped", mp.name)
149    return nil
150}
151
152func CheckHealth() bool {
153    mp.mu.RLock()
154    client := mp.client
155    mp.mu.RUnlock()
156
157    if client == nil {
158        return false
159    }
160
161    var status shared.HealthStatus
162    err := client.Call("PluginService.Health", struct{}{}, &status)
163
164    mp.mu.Lock()
165    mp.healthy =
166    mp.mu.Unlock()
167
168    return mp.healthy
169}
170
171func Process(data string, timeout time.Duration) {
172    mp.mu.RLock()
173    client := mp.client
174    healthy := mp.healthy
175    mp.mu.RUnlock()
176
177    if !healthy || client == nil {
178        return "", fmt.Errorf("plugin unhealthy")
179    }
180
181    req := &shared.ProcessRequest{Data: data}
182    resp := &shared.ProcessResponse{}
183
184    // Call with timeout
185    call := client.Go("PluginService.Process", req, resp, nil)
186
187    select {
188    case <-call.Done:
189        if call.Error != nil {
190            return "", call.Error
191        }
192        return resp.Result, nil
193    case <-time.After(timeout):
194        return "", fmt.Errorf("plugin call timeout")
195    }
196}
197
198type PluginManager struct {
199    plugins map[string]*ManagedPlugin
200    mu      sync.RWMutex
201}
202
203func NewPluginManager() *PluginManager {
204    return &PluginManager{
205        plugins: make(map[string]*ManagedPlugin),
206    }
207}
208
209func Register(name, address, binPath string) error {
210    plugin, err := NewManagedPlugin(name, address, binPath)
211    if err != nil {
212        return err
213    }
214
215    pm.mu.Lock()
216    pm.plugins[name] = plugin
217    pm.mu.Unlock()
218
219    return nil
220}
221
222func MonitorHealth(ctx context.Context, interval time.Duration) {
223    ticker := time.NewTicker(interval)
224    defer ticker.Stop()
225
226    for {
227        select {
228        case <-ticker.C:
229            pm.checkAllPlugins()
230        case <-ctx.Done():
231            return
232        }
233    }
234}
235
236func checkAllPlugins() {
237    pm.mu.RLock()
238    plugins := make([]*ManagedPlugin, 0, len(pm.plugins))
239    for _, p := range pm.plugins {
240        plugins = append(plugins, p)
241    }
242    pm.mu.RUnlock()
243
244    for _, plugin := range plugins {
245        if !plugin.CheckHealth() {
246            log.Printf("Plugin %s unhealthy, restarting...", plugin.name)
247            // Restart logic here
248        }
249    }
250}
251
252func Shutdown() {
253    pm.mu.Lock()
254    defer pm.mu.Unlock()
255
256    for _, plugin := range pm.plugins {
257        plugin.Stop()
258    }
259}

Exercise 3: Hot-Reloadable Plugin System

Difficulty: Expert | Time: 120-150 minutes | Learning Objectives: Master file system monitoring, implement graceful plugin lifecycle management, and build zero-downtime plugin updates.

Implement a plugin system that automatically reloads plugins when their files are modified without restarting the main application, enabling continuous development and deployment scenarios. This expert-level exercise teaches you to build development-friendly systems that support hot reloading—a critical feature for modern development workflows and real-time systems. You'll learn to implement file system watchers, manage plugin state gracefully during updates, and design APIs that can handle version transitions without service interruption. These patterns are essential for building IDEs, web servers, and any system where uptime is critical but code needs to be updated dynamically.

Solution - See "Hot-Reloading Strategies" section above

Combine the file watcher pattern with the versioned plugin manager to create a complete hot-reloadable system.

Exercise 4: Secure Plugin Sandbox

Difficulty: Expert | Time: 135-180 minutes | Learning Objectives: Master security sandboxing patterns, implement resource limits and monitoring, and build hardened plugin execution environments.

Create a secure plugin execution environment that limits resource usage, validates inputs/outputs, monitors plugin resource consumption, kills plugins that exceed limits, and logs all plugin activities for auditing. This expert-level exercise teaches you to build security-first plugin architectures that can safely execute untrusted third-party code—a critical pattern for SaaS platforms, gaming systems, and any application accepting user-contributed extensions. You'll learn to implement resource isolation, design security boundaries, and build monitoring systems that can detect and respond to malicious behavior. These security patterns are essential for modern platforms that balance extensibility with safety requirements.

Solution - See "Sandboxing and Security" section above

Combine the secure plugin runner with the input validator to create a hardened plugin system.

Exercise 5: Multi-Protocol Plugin Manager

Difficulty: Expert | Time: 150-180 minutes | Learning Objectives: Master adapter pattern implementation, build unified plugin interfaces, and create flexible plugin management systems.

Build a plugin manager that supports multiple plugin protocols through a unified interface, enabling applications to leverage different plugin technologies transparently. This capstone exercise teaches you to design sophisticated plugin architectures that can adapt to different requirements—performance with native plugins, isolation with RPC plugins, and security with WebAssembly plugins. You'll learn to implement the adapter pattern effectively, create unified abstractions over heterogeneous systems, and build extensible platforms that can evolve with changing requirements. These architectural patterns are essential for enterprise platforms, IDEs, and any system that needs to balance different plugin requirements seamlessly.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "sync"
  6)
  7
  8// Unified plugin interface
  9type Plugin interface {
 10    Name() string
 11    Version() string
 12    Process(input string)
 13    Close() error
 14}
 15
 16// Native plugin adapter
 17type NativePluginAdapter struct {
 18    name    string
 19    version string
 20    plugin  interface{}
 21}
 22
 23func Name() string {
 24    return a.name
 25}
 26
 27func Version() string {
 28    return a.version
 29}
 30
 31func Process(input string) {
 32    // Call native plugin method
 33    return fmt.Sprintf("Native: %s", input), nil
 34}
 35
 36func Close() error {
 37    // Native plugins can't be unloaded
 38    return nil
 39}
 40
 41// RPC plugin adapter
 42type RPCPluginAdapter struct {
 43    name    string
 44    version string
 45    address string
 46    // client *rpc.Client
 47}
 48
 49func Name() string {
 50    return a.name
 51}
 52
 53func Version() string {
 54    return a.version
 55}
 56
 57func Process(input string) {
 58    // Call RPC plugin method
 59    return fmt.Sprintf("RPC: %s", input), nil
 60}
 61
 62func Close() error {
 63    // Close RPC connection
 64    return nil
 65}
 66
 67// WebAssembly plugin adapter
 68type WasmPluginAdapter struct {
 69    name    string
 70    version string
 71    // instance *wasmer.Instance
 72}
 73
 74func Name() string {
 75    return a.name
 76}
 77
 78func Version() string {
 79    return a.version
 80}
 81
 82func Process(input string) {
 83    // Call Wasm function
 84    return fmt.Sprintf("Wasm: %s", input), nil
 85}
 86
 87func Close() error {
 88    // Cleanup Wasm instance
 89    return nil
 90}
 91
 92// Multi-protocol plugin manager
 93type MultiProtocolPluginManager struct {
 94    mu      sync.RWMutex
 95    plugins map[string]Plugin
 96}
 97
 98func NewMultiProtocolPluginManager() *MultiProtocolPluginManager {
 99    return &MultiProtocolPluginManager{
100        plugins: make(map[string]Plugin),
101    }
102}
103
104func LoadNativePlugin(name, path string) error {
105    // Load native .so plugin
106    adapter := &NativePluginAdapter{
107        name:    name,
108        version: "1.0.0",
109    }
110
111    m.mu.Lock()
112    m.plugins[name] = adapter
113    m.mu.Unlock()
114
115    return nil
116}
117
118func LoadRPCPlugin(name, address string) error {
119    // Connect to RPC plugin
120    adapter := &RPCPluginAdapter{
121        name:    name,
122        version: "1.0.0",
123        address: address,
124    }
125
126    m.mu.Lock()
127    m.plugins[name] = adapter
128    m.mu.Unlock()
129
130    return nil
131}
132
133func LoadWasmPlugin(name, path string) error {
134    // Load Wasm plugin
135    adapter := &WasmPluginAdapter{
136        name:    name,
137        version: "1.0.0",
138    }
139
140    m.mu.Lock()
141    m.plugins[name] = adapter
142    m.mu.Unlock()
143
144    return nil
145}
146
147func Get(name string) {
148    m.mu.RLock()
149    defer m.mu.RUnlock()
150
151    p, ok := m.plugins[name]
152    return p, ok
153}
154
155func Process(name, input string) {
156    plugin, ok := m.Get(name)
157    if !ok {
158        return "", fmt.Errorf("plugin %s not found", name)
159    }
160
161    return plugin.Process(input)
162}
163
164func Shutdown() error {
165    m.mu.Lock()
166    defer m.mu.Unlock()
167
168    for _, plugin := range m.plugins {
169        if err := plugin.Close(); err != nil {
170            return err
171        }
172    }
173
174    return nil
175}
176
177func main() {
178    manager := NewMultiProtocolPluginManager()
179
180    // Load different types of plugins
181    manager.LoadNativePlugin("native-plugin", "./plugin.so")
182    manager.LoadRPCPlugin("rpc-plugin", "localhost:9999")
183    manager.LoadWasmPlugin("wasm-plugin", "./plugin.wasm")
184
185    // Use plugins through unified interface
186    result, err := manager.Process("native-plugin", "test")
187    if err != nil {
188        fmt.Printf("Error: %v\n", err)
189    } else {
190        fmt.Printf("Result: %s\n", result)
191    }
192
193    // Cleanup
194    manager.Shutdown()
195}

Summary

💡 Key Takeaways:

  1. Plugin architecture creates ecosystems - Think app stores, not monolithic applications
  2. Choose the right approach for your needs - Native plugins for simple cases, RPC for production, WebAssembly for security
  3. Security matters - Untrusted plugins can compromise your entire application
  4. Plugin discovery is crucial - Automatic loading makes systems user-friendly

Choosing Your Plugin Architecture:

Native Go Plugins:

  • ✅ Best performance
  • ✅ Simple Go-to-Go communication
  • ✅ Easy to implement
  • ❌ Platform limitations
  • ❌ Version compatibility issues
  • ❌ No unloading capability

RPC-Based Plugins:

  • ✅ Cross-platform compatibility
  • ✅ Process isolation and security
  • ✅ Can restart failed plugins
  • ✅ Multiple language support
  • ❌ Performance overhead
  • ❌ More complex setup
  • ❌ Network serialization required

WebAssembly Plugins:

  • ✅ Maximum security sandbox
  • ✅ Cross-platform and multi-language
  • ✅ Resource limits and controls
  • ❌ Limited API surface
  • ❌ Performance overhead
  • ❌ Wasm learning curve

Real-world Production Checklist:

  • Define clear plugin interfaces and contracts
  • Implement plugin validation and security checks
  • Plan for version compatibility and migrations
  • Design plugin lifecycle management
  • Consider resource limits and monitoring
  • Document plugin development guidelines

When to Use Plugin Architecture:

  • ✅ Applications that need extensibility
  • ✅ Third-party developer ecosystems
  • ✅ Separating core from optional features
  • ✅ A/B testing different implementations
  • ✅ Multi-tenant customizations

When Plugin Architecture Might Be Overkill:

  • ❌ Simple CRUD applications
  • ❌ Single-developer projects
  • ❌ Fixed requirements
  • ❌ Performance-critical tight loops

Security Best Practices:

  • Always validate plugin inputs and outputs
  • Run untrusted plugins in sandboxes or separate processes
  • Implement resource limits
  • Use signed plugins for verification
  • Log all plugin activities for auditing

Next Steps:

  • Start with a simple plugin interface design
  • Choose the right plugin approach for your use case
  • Implement basic plugin discovery and loading
  • Add security and monitoring gradually
  • Consider contributing to existing plugin ecosystems

Plugin architecture transforms applications from static tools into dynamic platforms. Start simple, focus on security, and build ecosystems that empower users and developers alike.