Time: 45 minutes | Level: Advanced | Docker:docker run -it --rm golang:1.22 sh
Overview
Build a plugin system using Go's plugin package (-buildmode=plugin) and the hashicorp/go-plugin RPC-based alternative. Learn plugin contracts, limitations, and production patterns.
Step 1: Go Plugin Basics
Go Plugin Architecture:
Main App ──► plugin.Open("greeter.so") ──► plugin.Lookup("Plugin")
──► type assertion to interface
──► call methods
Limitations of plugin package:
Requires CGO + glibc (not Alpine/musl)
Plugin and host must be compiled with same Go version
Plugin cannot be unloaded
Not supported on Windows
💡 Use docker run -it --rm golang:1.22 sh (Debian/glibc) instead of Alpine for plugin support.
// contract/contract.go (shared package)
package contract
// Greeter is the plugin interface contract
// Both host and plugin must use this same interface
type Greeter interface {
Greet(name string) string
Lang() string
}
// Transformer transforms strings
type Transformer interface {
Transform(input string) string
Name() string
}
// plugins/english/english.go
package main
import "fmt"
type EnglishGreeter struct{}
func (g EnglishGreeter) Greet(name string) string {
return fmt.Sprintf("Hello, %s! Nice to meet you.", name)
}
func (g EnglishGreeter) Lang() string { return "English" }
// Plugin is the exported symbol the host will look up
var Plugin EnglishGreeter
# Build as shared object
cd plugins/english
go build -buildmode=plugin -o english.so .
// plugins/spanish/spanish.go
package main
import "fmt"
type SpanishGreeter struct{}
func (g SpanishGreeter) Greet(name string) string {
return fmt.Sprintf("¡Hola, %s! Mucho gusto.", name)
}
func (g SpanishGreeter) Lang() string { return "Español" }
var Plugin SpanishGreeter
go build -buildmode=plugin -o spanish.so .
// main.go (host)
package main
import (
"fmt"
"os"
"plugin"
)
// Greeter interface (must match plugin's implementation)
type Greeter interface {
Greet(name string) string
Lang() string
}
func loadPlugin(path string) (Greeter, error) {
p, err := plugin.Open(path)
if err != nil {
return nil, fmt.Errorf("open plugin %s: %w", path, err)
}
sym, err := p.Lookup("Plugin")
if err != nil {
return nil, fmt.Errorf("lookup Plugin symbol: %w", err)
}
g, ok := sym.(Greeter)
if !ok {
return nil, fmt.Errorf("Plugin does not implement Greeter interface")
}
return g, nil
}
func main() {
plugins := []string{"english.so", "spanish.so"}
for _, path := range plugins {
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("Plugin not found: %s\n", path)
continue
}
g, err := loadPlugin(path)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("[%s] %s\n", g.Lang(), g.Greet("World"))
}
}
docker run --rm golang:1.22 sh -c "
mkdir -p /tmp/plugin_lab/greeter /tmp/plugin_lab/mainapp
# Plugin
cat > /tmp/plugin_lab/greeter/greeter.go << 'GOEOF'
package main
import \"fmt\"
type greeterImpl struct{}
func (g greeterImpl) Greet(name string) string {
return fmt.Sprintf(\"Hello, %s! (from plugin)\", name)
}
func (g greeterImpl) Lang() string { return \"English\" }
var Plugin greeterImpl
GOEOF
cd /tmp/plugin_lab/greeter && go mod init greeter
go build -buildmode=plugin -o /tmp/plugin_lab/greeter.so . 2>&1
echo 'Plugin built:' \$(ls -lh /tmp/plugin_lab/greeter.so | awk '{print \$5}')
# Host
cat > /tmp/plugin_lab/mainapp/main.go << 'GOEOF'
package main
import (
\"fmt\"
\"plugin\"
)
type Greeter interface {
Greet(name string) string
Lang() string
}
func main() {
p, err := plugin.Open(\"/tmp/plugin_lab/greeter.so\")
if err != nil { panic(err) }
sym, err := p.Lookup(\"Plugin\")
if err != nil { panic(err) }
g, ok := sym.(Greeter)
if !ok { panic(\"type assertion failed\") }
fmt.Printf(\"[%s] %s\n\", g.Lang(), g.Greet(\"World\"))
}
GOEOF
cd /tmp/plugin_lab/mainapp && go mod init mainapp
go run main.go 2>&1"