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
- Docker Plugin System - Storage, network, and authorization plugins
- Kubernetes Operators - Custom controllers via CRDs
- VS Code Extensions - Language servers, themes, debuggers
- WordPress Plugins - PHP-based plugin ecosystem
- 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:
- Start Simple: Begin with interface-based plugins before moving to dynamic loading
- Version Carefully: Semantic versioning prevents incompatibility issues
- Security First: Validate, sandbox, and monitor all plugins
- Documentation: Clear plugin contracts make integration easier
- Testing: Test plugin loading, isolation, and failure scenarios
- Monitoring: Track plugin performance and health
- 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:
- Plugin architecture creates ecosystems - Think app stores, not monolithic applications
- Choose the right approach for your needs - Native plugins for simple cases, RPC for production, WebAssembly for security
- Security matters - Untrusted plugins can compromise your entire application
- 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.