Go+Wasm Plugin System: 5 Practical Patterns from Dynamic Loading to Secure Sandbox
Your Go Plugin Breaks on a Different Machine
You build a plugin system with Go's plugin package, only to discover: Linux-only support, macOS and Windows throw errors immediately. You switch to DLL dynamic loading, only to fall into the DLL Hell version compatibility nightmare. You use gRPC for inter-process plugin communication, but the latency is high, deployment is heavy, and a simple plugin requires a separate process. Worse yet—plugin crashes bring down the entire host process, making security isolation virtually nonexistent.
In 2026, the Go+Wasm plugin system has become the mainstream approach. The Wazero runtime (pure Go, no CGO dependency) lets you safely load Wasm modules within the host process. Plugins run in sandboxes they cannot escape, cross-platform compatibility requires zero adaptation, and hot reloading achieves zero downtime. This guide walks you through 5 practical patterns, from interface definition to secure sandbox, from host-guest communication to hot reload, with complete code throughout.
Go+Wasm Plugin System Core Concepts
| Concept | Description |
|---|---|
| WebAssembly (Wasm) | Portable binary instruction format that executes safely in a sandbox |
| Wazero | Pure Go Wasm runtime with no CGO dependency, supports WASI and Component Model |
| Host | The Go main program that loads and runs Wasm modules |
| Guest | The loaded Wasm module running inside a sandbox |
| Plugin Interface | Function calling convention between host and guest, defining exports and imports |
| Capability-based Security | Permission model where plugins can only access explicitly granted resources |
| Sandbox | Wasm's linear memory isolation model; guests cannot access host memory |
| WASI | WebAssembly System Interface, providing standardized filesystem, network, and other syscalls |
| Linear Memory | Wasm module's contiguous memory space; host reads/writes guest memory via offsets |
| Hot Reload | Runtime replacement of plugin modules without restarting the host process |
Go+Wasm Plugin System Architecture
┌─────────────────────────────────────────────┐
│ Go Host Process │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Plugin │ │ Plugin │ │ Plugin │ │
│ │ Manager │ │ Registry │ │ Loader │ │
│ └────┬────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ┌────▼────────────▼──────────────▼─────┐ │
│ │ Wazero Runtime │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Wasm │ │ Wasm │ ... │ │
│ │ │ Module A │ │ Module B │ │ │
│ │ │ (Sandbox)│ │ (Sandbox)│ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Problem Analysis: 5 Challenges of Go Plugin Systems
- Go plugin cross-platform limitations: The
pluginpackage only supports Linux-buildmode=plugin; macOS/Windows are unsupported, Go versions must match exactly, making it nearly unusable in production - Wasm binary compatibility: Different compilers (TinyGo/Go/WAT) produce Wasm modules with inconsistent interfaces, function signatures, and memory layouts
- Host-guest memory communication: Wasm only supports i32/i64/f32/f64 basic types; strings and complex structures require manual serialization through linear memory, which is error-prone
- Plugin security isolation: Plugins may contain malicious code; you need to restrict filesystem access, network access, CPU and memory usage to prevent resource exhaustion attacks
- Hot reload with zero downtime: Production plugin updates cannot restart the service; you need graceful replacement of running modules while handling in-flight requests
Step-by-Step: 5 Practical Patterns
Pattern 1: Plugin Interface Definition
package plugin
type PluginMeta struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
Author string `json:"author"`
}
type PluginContext struct {
RequestID string
Headers map[string]string
Parameters map[string]string
}
type PluginResult struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Error string `json:"error,omitempty"`
}
type Plugin interface {
Meta() PluginMeta
Init(ctx PluginContext) error
Execute(ctx PluginContext) (PluginResult, error)
Destroy() error
}
type CapabilitySet struct {
AllowFileSystem bool `json:"allowFileSystem"`
AllowNetwork bool `json:"allowNetwork"`
AllowEnv bool `json:"allowEnv"`
MaxMemoryMB uint32 `json:"maxMemoryMB"`
AllowedPaths []string `json:"allowedPaths"`
AllowedHosts []string `json:"allowedHosts"`
}
type PluginConfig struct {
Meta PluginMeta `json:"meta"`
WasmPath string `json:"wasmPath"`
Capabilities CapabilitySet `json:"capabilities"`
Timeout uint32 `json:"timeout"`
}
Pattern 2: Wasm Guest Module Implementation (TinyGo Compilation)
package main
import (
"encoding/json"
"unsafe"
)
type pluginContext struct {
RequestID string `json:"request_id"`
Headers map[string]string `json:"headers"`
Parameters map[string]string `json:"parameters"`
}
type pluginResult struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Error string `json:"error,omitempty"`
}
var memoryBuffer []byte
//export allocate
func allocate(size int32) unsafe.Pointer {
memoryBuffer = make([]byte, size)
return unsafe.Pointer(&memoryBuffer[0])
}
//export deallocate
func deallocate(ptr unsafe.Pointer, size int32) {
memoryBuffer = nil
}
//export meta
func meta() unsafe.Pointer {
m := map[string]string{
"name": "json-transformer",
"version": "1.0.0",
"description": "Transform JSON data with custom rules",
"author": "toolsku",
}
data, _ := json.Marshal(m)
memoryBuffer = data
return unsafe.Pointer(&memoryBuffer[0])
}
//export execute
func execute(ctxPtr unsafe.Pointer, ctxLen int32) unsafe.Pointer {
ctxData := memoryBuffer[:ctxLen]
var ctx pluginContext
if err := json.Unmarshal(ctxData, &ctx); err != nil {
result := pluginResult{
Success: false,
Error: err.Error(),
}
data, _ := json.Marshal(result)
memoryBuffer = data
return unsafe.Pointer(&memoryBuffer[0])
}
transformed := make(map[string]interface{})
for k, v := range ctx.Parameters {
transformed[k] = v
}
transformed["_request_id"] = ctx.RequestID
transformed["_processed"] = true
result := pluginResult{
Success: true,
Data: transformed,
}
data, _ := json.Marshal(result)
memoryBuffer = data
return unsafe.Pointer(&memoryBuffer[0])
}
func main() {}
Build command:
tinygo build -o plugin.wasm -target=wasi -no-debug -scheduler=none .
Pattern 3: Wazero Host Runtime Setup
package host
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
type WasmPlugin struct {
Name string
Version string
runtime wazero.Runtime
module api.Module
compiled wazero.CompiledModule
config PluginConfig
}
type PluginManager struct {
runtime wazero.Runtime
plugins map[string]*WasmPlugin
ctx context.Context
}
func NewPluginManager() *PluginManager {
ctx := context.Background()
rt := wazero.NewRuntime(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, rt)
return &PluginManager{
runtime: rt,
plugins: make(map[string]*WasmPlugin),
ctx: ctx,
}
}
func (pm *PluginManager) LoadPlugin(config PluginConfig) (*WasmPlugin, error) {
wasmBytes, err := os.ReadFile(config.WasmPath)
if err != nil {
return nil, fmt.Errorf("read wasm file: %w", err)
}
compiled, err := pm.runtime.CompileModule(pm.ctx, wasmBytes)
if err != nil {
return nil, fmt.Errorf("compile wasm: %w", err)
}
moduleConfig := wazero.NewModuleConfig().
WithName(config.Meta.Name).
WithEnv("PLUGIN_VERSION", config.Meta.Version)
if config.Capabilities.MaxMemoryMB > 0 {
moduleConfig = moduleConfig.WithMemoryLimitPages(
config.Capabilities.MaxMemoryMB * 1024 * 1024 / 65536,
)
}
mod, err := pm.runtime.InstantiateModule(pm.ctx, compiled, moduleConfig)
if err != nil {
return nil, fmt.Errorf("instantiate module: %w", err)
}
plugin := &WasmPlugin{
Name: config.Meta.Name,
Version: config.Meta.Version,
runtime: pm.runtime,
module: mod,
compiled: compiled,
config: config,
}
pm.plugins[config.Meta.Name] = plugin
return plugin, nil
}
func (pm *PluginManager) GetPlugin(name string) (*WasmPlugin, bool) {
p, ok := pm.plugins[name]
return p, ok
}
func (pm *PluginManager) UnloadPlugin(name string) error {
plugin, ok := pm.plugins[name]
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
if err := plugin.module.Close(pm.ctx); err != nil {
return fmt.Errorf("close module: %w", err)
}
delete(pm.plugins, name)
return nil
}
func (pm *PluginManager) Close() error {
for name := range pm.plugins {
_ = pm.UnloadPlugin(name)
}
return pm.runtime.Close(pm.ctx)
}
Pattern 4: Host-Guest Communication (Memory Sharing and Function Calls)
package host
import (
"encoding/json"
"fmt"
)
func readMemory(mod api.Module, offset uint32, size uint32) []byte {
buf := make([]byte, size)
ok := mod.Memory().Read(offset, buf)
if !ok {
return nil
}
return buf
}
func writeString(mod api.Module, data string) (uint32, error) {
allocate := mod.ExportedFunction("allocate")
if allocate == nil {
return 0, fmt.Errorf("allocate function not found")
}
results, err := allocate.Call(nil, uint64(len(data)))
if err != nil {
return 0, fmt.Errorf("allocate memory: %w", err)
}
offset := uint32(results[0])
ok := mod.Memory().Write(offset, []byte(data))
if !ok {
return 0, fmt.Errorf("write memory failed")
}
return offset, nil
}
func (p *WasmPlugin) CallMeta() (map[string]string, error) {
metaFn := p.module.ExportedFunction("meta")
if metaFn == nil {
return nil, fmt.Errorf("meta function not found")
}
results, err := metaFn.Call(nil)
if err != nil {
return nil, fmt.Errorf("call meta: %w", err)
}
offset := uint32(results[0])
data := readMemory(p.module, offset, 512)
var meta map[string]string
if err := json.Unmarshal(data, &meta); err != nil {
return nil, fmt.Errorf("unmarshal meta: %w", err)
}
return meta, nil
}
func (p *WasmPlugin) CallExecute(ctx PluginContext) (PluginResult, error) {
executeFn := p.module.ExportedFunction("execute")
if executeFn == nil {
return PluginResult{}, fmt.Errorf("execute function not found")
}
ctxJSON, err := json.Marshal(ctx)
if err != nil {
return PluginResult{}, fmt.Errorf("marshal context: %w", err)
}
offset, err := writeString(p.module, string(ctxJSON))
if err != nil {
return PluginResult{}, fmt.Errorf("write context: %w", err)
}
results, err := executeFn.Call(nil, uint64(offset), uint64(len(ctxJSON)))
if err != nil {
return PluginResult{}, fmt.Errorf("call execute: %w", err)
}
resultOffset := uint32(results[0])
resultData := readMemory(p.module, resultOffset, 4096)
var result PluginResult
if err := json.Unmarshal(resultData, &result); err != nil {
return PluginResult{}, fmt.Errorf("unmarshal result: %w", err)
}
return result, nil
}
Pattern 5: Capability-Based Security
package host
import (
"context"
"fmt"
"net"
"strings"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
type CapabilityEnforcer struct {
capabilities CapabilitySet
}
func NewCapabilityEnforcer(cap CapabilitySet) *CapabilityEnforcer {
return &CapabilityEnforcer{capabilities: cap}
}
func (ce *CapabilityEnforcer) BuildFSConfig() wazero.FSConfig {
fsConfig := wazero.NewFSConfig()
if !ce.capabilities.AllowFileSystem {
return fsConfig
}
for _, path := range ce.capabilities.AllowedPaths {
fsConfig = fsConfig.WithDirMount(path, path)
}
return fsConfig
}
func (ce *CapabilityEnforcer) BuildModuleConfig(config PluginConfig) wazero.ModuleConfig {
moduleConfig := wazero.NewModuleConfig().
WithName(config.Meta.Name)
if ce.capabilities.MaxMemoryMB > 0 {
pages := uint32(ce.capabilities.MaxMemoryMB * 1024 * 1024 / 65536)
if pages > 256 {
pages = 256
}
moduleConfig = moduleConfig.WithMemoryLimitPages(pages)
}
if !ce.capabilities.AllowEnv {
moduleConfig = moduleConfig.WithEnv("PATH", "")
}
return moduleConfig
}
func (ce *CapabilityEnforcer) CheckNetworkAccess(host string) error {
if !ce.capabilities.AllowNetwork {
return fmt.Errorf("network access denied: plugin has no network capability")
}
if len(ce.capabilities.AllowedHosts) == 0 {
return nil
}
for _, allowed := range ce.capabilities.AllowedHosts {
if host == allowed || strings.HasSuffix(host, "."+allowed) {
return nil
}
}
return fmt.Errorf("network access denied: host %s not in allowed list", host)
}
func (ce *CapabilityEnforcer) RegisterHostFunctions(rt wazero.Runtime) error {
_, err := rt.NewHostModuleBuilder("env").
NewFunctionBuilder().
WithFunc(func(ctx context.Context, m api.Module, hostPtr uint32, hostLen uint32, port uint32) uint32 {
hostBytes := readMemory(m, hostPtr, hostLen)
host := string(hostBytes)
if err := ce.CheckNetworkAccess(host); err != nil {
return 0
}
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return 0
}
conn.Close()
return 1
}).
Export("net_dial").
NewFunctionBuilder().
WithFunc(func(ctx context.Context, m api.Module, pathPtr uint32, pathLen uint32) uint32 {
if !ce.capabilities.AllowFileSystem {
return 0
}
pathBytes := readMemory(m, pathPtr, pathLen)
path := string(pathBytes)
for _, allowed := range ce.capabilities.AllowedPaths {
if strings.HasPrefix(path, allowed) {
return 1
}
}
return 0
}).
Export("fs_check").
Instantiate(context.Background())
return err
}
Pattern 6: Hot Reload Mechanism
package host
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
type HotReloader struct {
manager *PluginManager
watcher *fsnotify.Watcher
hashes map[string]string
mu sync.RWMutex
stopCh chan struct{}
configs map[string]PluginConfig
onReload func(name string, plugin *WasmPlugin)
}
func NewHotReloader(manager *PluginManager) (*HotReloader, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("create watcher: %w", err)
}
return &HotReloader{
manager: manager,
watcher: watcher,
hashes: make(map[string]string),
stopCh: make(chan struct{}),
configs: make(map[string]PluginConfig),
}, nil
}
func (hr *HotReloader) Watch(config PluginConfig) error {
dir := filepath.Dir(config.WasmPath)
if err := hr.watcher.Add(dir); err != nil {
return fmt.Errorf("watch directory: %w", err)
}
hash, err := hr.computeHash(config.WasmPath)
if err != nil {
return fmt.Errorf("compute hash: %w", err)
}
hr.mu.Lock()
hr.hashes[config.WasmPath] = hash
hr.configs[config.Meta.Name] = config
hr.mu.Unlock()
return nil
}
func (hr *HotReloader) OnReload(fn func(name string, plugin *WasmPlugin)) {
hr.onReload = fn
}
func (hr *HotReloader) Start() {
debounce := make(map[string]time.Time)
go func() {
for {
select {
case <-hr.stopCh:
return
case event, ok := <-hr.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
name := hr.findPluginByPath(event.Name)
if name == "" {
continue
}
if last, exists := debounce[event.Name]; exists && time.Since(last) < 500*time.Millisecond {
continue
}
debounce[event.Name] = time.Now()
time.Sleep(300 * time.Millisecond)
if err := hr.reloadPlugin(name); err != nil {
fmt.Printf("hot reload failed for %s: %v\n", name, err)
}
case <-hr.watcher.Errors:
}
}
}()
}
func (hr *HotReloader) Stop() {
close(hr.stopCh)
hr.watcher.Close()
}
func (hr *HotReloader) reloadPlugin(name string) error {
hr.mu.RLock()
config, exists := hr.configs[name]
hr.mu.RUnlock()
if !exists {
return fmt.Errorf("config not found for %s", name)
}
newHash, err := hr.computeHash(config.WasmPath)
if err != nil {
return fmt.Errorf("compute new hash: %w", err)
}
hr.mu.RLock()
oldHash := hr.hashes[config.WasmPath]
hr.mu.RUnlock()
if newHash == oldHash {
return nil
}
if err := hr.manager.UnloadPlugin(name); err != nil {
return fmt.Errorf("unload old plugin: %w", err)
}
plugin, err := hr.manager.LoadPlugin(config)
if err != nil {
return fmt.Errorf("load new plugin: %w", err)
}
hr.mu.Lock()
hr.hashes[config.WasmPath] = newHash
hr.mu.Unlock()
if hr.onReload != nil {
hr.onReload(name, plugin)
}
fmt.Printf("plugin %s hot-reloaded (hash: %s)\n", name, newHash[:8])
return nil
}
func (hr *HotReloader) computeHash(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}
func (hr *HotReloader) findPluginByPath(path string) string {
hr.mu.RLock()
defer hr.mu.RUnlock()
for name, config := range hr.configs {
if config.WasmPath == path {
return name
}
}
return ""
}
Complete usage example:
package main
import (
"fmt"
"log"
"github.com/yourorg/plugin-host/host"
)
func main() {
manager := host.NewPluginManager()
defer manager.Close()
config := host.PluginConfig{
Meta: host.PluginMeta{
Name: "json-transformer",
Version: "1.0.0",
Description: "Transform JSON data with custom rules",
Author: "toolsku",
},
WasmPath: "./plugins/json-transformer.wasm",
Capabilities: host.CapabilitySet{
AllowFileSystem: false,
AllowNetwork: false,
AllowEnv: false,
MaxMemoryMB: 32,
AllowedPaths: []string{},
AllowedHosts: []string{},
},
Timeout: 5000,
}
plugin, err := manager.LoadPlugin(config)
if err != nil {
log.Fatalf("load plugin: %v", err)
}
meta, err := plugin.CallMeta()
if err != nil {
log.Fatalf("call meta: %v", err)
}
fmt.Printf("Plugin: %s v%s\n", meta["name"], meta["version"])
ctx := host.PluginContext{
RequestID: "req-001",
Headers: map[string]string{"Content-Type": "application/json"},
Parameters: map[string]string{"action": "transform", "format": "camelCase"},
}
result, err := plugin.CallExecute(ctx)
if err != nil {
log.Fatalf("call execute: %v", err)
}
fmt.Printf("Result: %+v\n", result)
reloader, err := host.NewHotReloader(manager)
if err != nil {
log.Fatalf("create reloader: %v", err)
}
reloader.OnReload(func(name string, p *host.WasmPlugin) {
fmt.Printf("Plugin %s reloaded successfully\n", name)
})
if err := reloader.Watch(config); err != nil {
log.Fatalf("watch plugin: %v", err)
}
reloader.Start()
defer reloader.Stop()
select {}
}
Pitfall Guide
Pitfall 1: Passing Go Strings Directly to Wasm
// ❌ Wrong: Go strings contain pointers, cannot be passed directly to Wasm
result, err := metaFn.Call(nil, uint64(uintptr(unsafe.Pointer(&goStr))))
// ✅ Correct: Allocate guest memory via allocate, write byte data
offset, err := writeString(p.module, string(ctxJSON))
results, err := executeFn.Call(nil, uint64(offset), uint64(len(ctxJSON)))
Pitfall 2: Ignoring Wasm Memory Alignment
// ❌ Wrong: Read from returned offset without considering data length
data := readMemory(p.module, offset, 0)
// ✅ Correct: Convention that return values include offset and length, or use fixed buffer size
results, err := metaFn.Call(nil)
offset := uint32(results[0])
length := uint32(results[1])
data := readMemory(p.module, offset, length)
Pitfall 3: TinyGo Build Without -scheduler=none
# ❌ Wrong: Default scheduler causes goroutine leaks in Wasm runtime
tinygo build -o plugin.wasm -target=wasi .
# ✅ Correct: Disable scheduler to avoid goroutine scheduling issues
tinygo build -o plugin.wasm -target=wasi -no-debug -scheduler=none .
Pitfall 4: Not Limiting Wasm Module Memory
// ❌ Wrong: No memory limit, malicious plugins can exhaust host memory
moduleConfig := wazero.NewModuleConfig().WithName("plugin")
// ✅ Correct: Set memory page limit (1 page = 64KB, 32MB = 512 pages)
moduleConfig := wazero.NewModuleConfig().
WithName("plugin").
WithMemoryLimitPages(512)
Pitfall 5: Hot Reload Without Waiting for Write Completion
// ❌ Wrong: Trigger reload during file write, loading incomplete Wasm
case event := <-hr.watcher.Events:
hr.reloadPlugin(name)
// ✅ Correct: Debounce + delay to ensure file write completion
case event := <-hr.watcher.Events:
debounce[event.Name] = time.Now()
time.Sleep(300 * time.Millisecond)
hr.reloadPlugin(name)
Error Troubleshooting
| # | Error Message | Cause | Solution |
|---|---|---|---|
| 1 | module compiled with a different version of Go |
Go plugin package requires exact Go version match between host and plugin | Switch to Wasm approach, avoid Go plugin package |
| 2 | plugin was built with a different version of package |
Go plugin dependency version mismatch | Use Wasm isolated compilation to eliminate version coupling |
| 3 | out of bounds memory access |
Host reads/writes Wasm memory offset out of bounds | Check allocate return value and memory length calculation |
| 4 | function export not found: allocate |
Wasm module does not export allocate function | Ensure TinyGo exports //export allocate during compilation |
| 5 | wasm validation error: invalid section |
Wasm binary file corrupted or incorrect format | Recompile Wasm module, check build command |
| 6 | instantiation error: memory size exceeds limit |
Module initial memory exceeds limit | Increase WithMemoryLimitPages or optimize module memory usage |
| 7 | goroutine stack overflow in wasm |
TinyGo default scheduler causes stack overflow | Add -scheduler=none during compilation |
| 8 | import not found: wasi_snapshot_preview1 |
WASI module not initialized | Call wasi_snapshot_preview1.MustInstantiate |
| 9 | context deadline exceeded |
Plugin execution timeout | Use context.WithTimeout to control execution time |
| 10 | file already closed during hot reload |
Old module not properly closed during hot reload | Ensure old module is Closed before loading new module |
Advanced Optimization
1. Wasm Component Model Interface Specification
package host
import (
"github.com/tetratelabs/wazero"
)
type ComponentInterface struct {
Exports []FunctionSignature `json:"exports"`
Imports []FunctionSignature `json:"imports"`
}
type FunctionSignature struct {
Name string `json:"name"`
Params []string `json:"params"`
Results []string `json:"results"`
}
func ValidateComponent(rt wazero.Runtime, wasmBytes []byte) (*ComponentInterface, error) {
compiled, err := rt.CompileModule(context.Background(), wasmBytes)
if err != nil {
return nil, err
}
ci := &ComponentInterface{}
for _, exp := range compiled.ExportedFunctions() {
sig := FunctionSignature{
Name: exp.Name(),
}
for _, param := range exp.ParamTypes() {
sig.Params = append(sig.Params, param.String())
}
for _, result := range exp.ResultTypes() {
sig.Results = append(sig.Results, result.String())
}
ci.Exports = append(ci.Exports, sig)
}
return ci, nil
}
2. Plugin Pool and Concurrent Scheduling
package host
import (
"context"
"sync"
)
type PluginPool struct {
manager *PluginManager
config PluginConfig
pool chan *WasmPlugin
mu sync.Mutex
size int
}
func NewPluginPool(manager *PluginManager, config PluginConfig, poolSize int) (*PluginPool, error) {
pool := &PluginPool{
manager: manager,
config: config,
pool: make(chan *WasmPlugin, poolSize),
size: poolSize,
}
for i := 0; i < poolSize; i++ {
plugin, err := manager.LoadPlugin(PluginConfig{
Meta: PluginMeta{Name: fmt.Sprintf("%s-%d", config.Meta.Name, i), Version: config.Meta.Version},
WasmPath: config.WasmPath,
Capabilities: config.Capabilities,
Timeout: config.Timeout,
})
if err != nil {
return nil, err
}
pool.pool <- plugin
}
return pool, nil
}
func (pp *PluginPool) Acquire(ctx context.Context) (*WasmPlugin, error) {
select {
case plugin := <-pp.pool:
return plugin, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (pp *PluginPool) Release(plugin *WasmPlugin) {
pp.pool <- plugin
}
func (pp *PluginPool) Execute(ctx context.Context, pCtx PluginContext) (PluginResult, error) {
plugin, err := pp.Acquire(ctx)
if err != nil {
return PluginResult{}, err
}
defer pp.Release(plugin)
execCtx, cancel := context.WithTimeout(ctx, time.Duration(pp.config.Timeout)*time.Millisecond)
defer cancel()
resultCh := make(chan PluginResult, 1)
errCh := make(chan error, 1)
go func() {
result, err := plugin.CallExecute(pCtx)
if err != nil {
errCh <- err
return
}
resultCh <- result
}()
select {
case result := <-resultCh:
return result, nil
case err := <-errCh:
return PluginResult{}, err
case <-execCtx.Done():
return PluginResult{}, fmt.Errorf("plugin execution timeout")
}
}
3. Plugin Metrics Monitoring and Circuit Breaking
package host
import (
"sync"
"sync/atomic"
"time"
)
type PluginMetrics struct {
Name string
CallCount atomic.Int64
ErrorCount atomic.Int64
TotalDuration atomic.Int64
LastCallTime atomic.Int64
CircuitOpen atomic.Bool
consecutiveFails atomic.Int64
mu sync.Mutex
threshold int64
resetTimeout time.Duration
lastFailTime time.Time
}
func NewPluginMetrics(name string, failThreshold int64, resetTimeout time.Duration) *PluginMetrics {
return &PluginMetrics{
Name: name,
threshold: failThreshold,
resetTimeout: resetTimeout,
}
}
func (m *PluginMetrics) RecordCall(duration time.Duration, err error) {
m.CallCount.Add(1)
m.TotalDuration.Add(int64(duration))
m.LastCallTime.Store(time.Now().UnixMilli())
if err != nil {
m.ErrorCount.Add(1)
fails := m.consecutiveFails.Add(1)
if fails >= m.threshold {
m.CircuitOpen.Store(true)
m.lastFailTime = time.Now()
}
} else {
m.consecutiveFails.Store(0)
}
}
func (m *PluginMetrics) AllowCall() bool {
if !m.CircuitOpen.Load() {
return true
}
m.mu.Lock()
defer m.mu.Unlock()
if time.Since(m.lastFailTime) > m.resetTimeout {
m.CircuitOpen.Store(false)
m.consecutiveFails.Store(0)
return true
}
return false
}
func (m *PluginMetrics) Snapshot() map[string]interface{} {
calls := m.CallCount.Load()
errors := m.ErrorCount.Load()
totalMs := m.TotalDuration.Load()
avgMs := float64(0)
if calls > 0 {
avgMs = float64(totalMs) / float64(calls) / float64(time.Millisecond)
}
return map[string]interface{}{
"name": m.Name,
"call_count": calls,
"error_count": errors,
"error_rate": float64(errors) / float64(calls),
"avg_duration_ms": avgMs,
"circuit_open": m.CircuitOpen.Load(),
}
}
Comparison Analysis
| Dimension | Go+Wasm (Wazero) | Go Plugin | gRPC Plugin | Lua Embedding | JavaScript Embedding |
|---|---|---|---|---|---|
| Cross-platform | ✅ All platforms | ❌ Linux only | ✅ All platforms | ✅ All platforms | ✅ All platforms |
| Security isolation | ✅ Sandbox isolation | ❌ Same process | ✅ Process isolation | ⚠️ Limited | ⚠️ Limited |
| Performance overhead | Low (~μs) | Lowest (ns) | High (ms) | Low (μs) | Medium (μs) |
| Memory safety | ✅ Linear memory | ❌ Shared memory | ✅ Independent memory | ❌ Shared memory | ❌ Shared memory |
| Hot reload | ✅ Native support | ❌ Requires restart | ⚠️ Process management | ✅ Native support | ✅ Native support |
| Language support | Multi-language Wasm | Go only | Any language | Lua only | JS only |
| Ecosystem maturity | ⚠️ Developing | ✅ Go native | ✅ Mature | ✅ Mature | ✅ Mature |
| Debugging experience | ⚠️ Limited | ✅ Go toolchain | ✅ Independent debug | ⚠️ Limited | ⚠️ Limited |
| Binary size | Small (~1MB) | Medium (~5MB) | Large (~20MB) | Small (~500KB) | Medium (~10MB) |
| CPU limiting | ✅ Possible | ❌ Not possible | ✅ Process-level | ❌ Not possible | ⚠️ Limited |
Summary: The Go+Wasm plugin system uses the Wazero runtime to create secure sandboxes within the host process. The 5 practical patterns build upon each other: 1) Plugin interface definition—unified calling conventions between host and guest; 2) TinyGo guest module compilation—export standard functions for host invocation; 3) Wazero host runtime—compile, instantiate, and manage Wasm modules; 4) Host-guest memory communication—share complex data through linear memory; 5) Capability security model—restrict filesystem, network, memory, and other permissions; 6) Hot reload mechanism—fsnotify monitoring + debounce + graceful replacement. Core principle: least privilege → memory isolation → capability control → graceful reload.
Recommended Tools
- JSON Formatter: /en/json/format
- Base64 Encoder/Decoder: /en/encode/base64
- Hash Calculator: /en/encode/hash
Try these browser-local tools — no sign-up required →