Appearance
Go Guide
Build Noorle plugins with Go.
Why Go?
Strengths
- Simple & Readable - Clean syntax with straightforward patterns
- Fast Compilation - Quick build times for rapid iteration
- Strong Typing - Catch errors at compile time without complexity
- TinyGo Support - Optimized WASM output with small binary sizes
- Great Tooling - Built-in formatting, testing, and benchmarking
Best For
- API integrations
- Data processing pipelines
- Microservices ports
- Network utilities
- System tools
Trade-offs
- No generics in TinyGo (yet)
- Limited standard library in WASM
- No goroutines in TinyGo WASM
Quick Start
Prerequisites
bash
# Install Noorle CLI
curl -L cli.noorle.dev | sh
Create Plugin
bash
# Initialize from template
noorle plugin init my-plugin --template go
cd my-plugin
# Install dependencies (Go, TinyGo, wasm tools)
noorle plugin prepare
# Build and deploy
noorle plugin publish
Minimal Example
go
package main
import (
"fmt"
component "github.com/example/my-plugin/gen"
"go.bytecodealliance.org/cm"
)
type Plugin struct{}
func (p Plugin) Process(input string) cm.Result[string, string] {
return cm.OK[string, string](fmt.Sprintf("Processed: %s", input))
}
func init() {
component.Exports.Set(Plugin{})
}
func main() {}
Project Structure
tree
my-plugin/
├── main.go # Main implementation
├── wit/world.wit # API definition
├── go.mod # Go dependencies
├── wkg.toml # WIT package config
├── build.sh # Build script
├── prepare.sh # Dependency installer
├── noorle.yaml # Plugin config (optional)
├── gen/ # Generated bindings
└── dist/ # Build output
├── plugin.wasm # Compiled component
└── *.npack # Deployment archive
File Descriptions
File | Purpose |
---|---|
main.go | Plugin implementation with exported functions |
wit/world.wit | WebAssembly interface definition |
go.mod | Go module dependencies |
wkg.toml | WIT dependency configuration |
build.sh | Compilation and component creation |
prepare.sh | Installs Go, TinyGo, and wasm tools |
Implementation Guide
Basic Plugin Structure
go
package main
import (
"fmt"
// Generated from your WIT file
component "github.com/example/my-plugin/gen"
"go.bytecodealliance.org/cm"
)
// Your plugin struct
type MyPluginComponent struct{}
// Implement exported functions
func (c MyPluginComponent) SayHi(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func (c MyPluginComponent) Add(a, b float64) float64 {
return a + b
}
// Register exports
func init() {
component.Exports.Set(MyPluginComponent{})
}
// Required main function
func main() {}
Working with WIT
Define your API in wit/world.wit
:
wit
package example:my-plugin;
world my-plugin-component {
/// Greet a person by name
export say-hi: func(name: string) -> string;
/// Add two numbers
export add: func(a: float64, b: float64) -> float64;
/// Process with error handling
export process: func(input: string) -> result<string, string>;
}
Go-specific binding:
go
import (
component "github.com/example/my-plugin/gen"
"go.bytecodealliance.org/cm"
)
type Plugin struct{}
func (p Plugin) Process(input string) cm.Result[string, string] {
// Result maps directly to WIT result type
if input == "" {
return cm.Err[string, string]("Input cannot be empty")
}
result := processData(input)
return cm.OK[string, string](result)
}
Error Handling
go
import "go.bytecodealliance.org/cm"
// Using Result types for error handling
func (c Component) SafeProcess(input string) cm.Result[string, string] {
// Validate input
if input == "" {
return cm.Err[string, string]("input cannot be empty")
}
// Process with error handling
result, err := processData(input)
if err != nil {
return cm.Err[string, string](err.Error())
}
return cm.OK[string, string](result)
}
// Helper function with standard Go errors
func processData(input string) (string, error) {
if len(input) > 1000 {
return "", fmt.Errorf("input too large: %d bytes", len(input))
}
return fmt.Sprintf("Processed: %s", input), nil
}
Environment Variables
go
import "github.com/example/my-plugin/gen/wasi/cli/environment"
func getConfig() map[string]string {
config := make(map[string]string)
envVars := environment.GetEnvironment()
for _, envVar := range envVars {
key := envVar.F0
value := envVar.F1
switch key {
case "API_KEY":
config["apiKey"] = value
case "DEBUG":
config["debug"] = value
case "SERVICE_URL":
config["serviceUrl"] = value
}
}
return config
}
JSON Processing
go
import "encoding/json"
type Request struct {
Command string `json:"command"`
Value float64 `json:"value"`
Options map[string]interface{} `json:"options,omitempty"`
}
type Response struct {
Success bool `json:"success"`
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
func (c Component) ProcessJson(input string) cm.Result[string, string] {
var req Request
if err := json.Unmarshal([]byte(input), &req); err != nil {
return cm.Err[string, string](fmt.Sprintf("Invalid JSON: %v", err))
}
// Process request
if req.Command == "" {
return cm.Err[string, string]("Command is required")
}
resp := Response{
Success: true,
Result: fmt.Sprintf("Executed %s with value %.2f", req.Command, req.Value),
}
result, err := json.Marshal(resp)
if err != nil {
return cm.Err[string, string](fmt.Sprintf("Marshal error: %v", err))
}
return cm.OK[string, string](string(result))
}
HTTP Requests (Optional)
Required WIT imports:
wit
world my-plugin-component {
import wasi:http/types@0.2.0;
import wasi:http/outgoing-handler@0.2.0;
export fetch-data: func(url: string) -> result<string, string>;
}
Implementation:
go
import (
outgoinghandler "github.com/example/my-plugin/gen/wasi/http/outgoing-handler"
"github.com/example/my-plugin/gen/wasi/http/types"
"github.com/example/my-plugin/gen/wasi/io/poll"
"go.bytecodealliance.org/cm"
)
func (c Component) FetchData(url string) cm.Result[string, string] {
// Create headers
headers := types.NewFields()
headers.Append("User-Agent", types.FieldValue(
cm.ToList([]uint8("my-plugin/1.0"))))
// Create request
request := types.NewOutgoingRequest(headers)
request.SetMethod(types.MethodGet())
request.SetScheme(cm.Some(types.SchemeHTTPS()))
request.SetAuthority(cm.Some("api.example.com"))
request.SetPathWithQuery(cm.Some("/data"))
// Send request
futureResponse := outgoinghandler.Handle(request,
cm.None[types.RequestOptions]())
// Poll for response
pollable := futureResponse.OK().Subscribe()
poll.Poll(cm.ToList([]types.Pollable{pollable}))
// Get response
response := futureResponse.OK().Get()
if response.IsErr() {
return cm.Err[string, string]("Request failed")
}
// Read body
body := response.OK().Body()
// ... process body stream ...
return cm.OK[string, string](string(bodyBytes))
}
Required permissions:
yaml
permissions:
network:
allow:
- host: "api.example.com"
Testing
Local Testing with Wasmtime
bash
# Build the plugin
noorle plugin build
# Test with WAVE format
wasmtime run --invoke 'say_hi("World")' dist/plugin.wasm
wasmtime run --invoke 'add(2, 3)' dist/plugin.wasm
wasmtime run --invoke 'process("test data")' dist/plugin.wasm
# With environment variables
wasmtime run --env API_KEY=test123 --invoke 'process("test")' dist/plugin.wasm
Language-Specific Features
TinyGo Optimizations
TinyGo produces smaller WASM binaries than standard Go:
go
// go.mod - TinyGo compatible
module github.com/example/my-plugin
go 1.23.0
require go.bytecodealliance.org/cm v0.3.0
Component Model Types
go
// Using cm package for component model types
import "go.bytecodealliance.org/cm"
// Option types
func (c Component) MaybeProcess(input string) cm.Option[string] {
if input == "" {
return cm.None[string]()
}
return cm.Some(processData(input))
}
// List types
func (c Component) ProcessList(items cm.List[string]) cm.List[string] {
results := make([]string, 0, items.Len())
for i := 0; i < items.Len(); i++ {
results = append(results, process(items.At(i)))
}
return cm.ToList(results)
}
Generated Bindings
go
// Bindings are generated from WIT files
// Located in gen/ directory after build
// Access generated types
import "github.com/example/my-plugin/gen"
// Use generated interfaces
type MyPlugin struct{}
func init() {
gen.Exports.Set(MyPlugin{})
}
Build Configuration
Dependencies
go.mod:
go
module github.com/example/my-plugin
go 1.23.0
require (
go.bytecodealliance.org/cm v0.3.0 // Component model support
)
wkg.toml:
toml
# WIT package dependencies
[dependencies]
"wasi:http" = "0.2.0"
"wasi:cli" = "0.2.0"
Build Optimization
TinyGo automatically optimizes for size. Additional settings:
bash
# Build flags in build.sh (handled by noorle plugin build)
# -opt=z for size optimization
# -no-debug to strip debug info
Build Process
bash
# Build your plugin (handles compilation, optimization, packaging)
noorle plugin build
The build process:
- Generates Go bindings from WIT
- Compiles with TinyGo to wasm32-wasip2
wkg
fetches WIT dependencies from imports- Creates component with wasm-tools
- Optimizes output
- Packages into .npack archive
Troubleshooting
Common Issues
Build Fails
Problem: wit-bindgen-go: command not found
Solution:
bash
noorle plugin prepare # Installs all required tools
Runtime Error
Problem: failed to find export 'process'
Solution:
go
// Ensure init() registers exports
func init() {
component.Exports.Set(MyPlugin{})
}
// Ensure main() exists
func main() {}
TinyGo Limitations
Problem: package x not supported by TinyGo
Solution:
go
// Use alternatives or implement needed functionality
// Check TinyGo compatibility: https://tinygo.org/docs/reference/lang-support/
Debugging
Enable debug output:
bash
DEBUG=1 noorle plugin build
Check generated bindings:
bash
ls -la gen/
View WASM exports:
bash
wasm-tools component wit dist/plugin.wasm
Performance
Optimization tips:
- Keep dependencies minimal
- Use TinyGo for smaller binaries
- Avoid reflection when possible
- Pre-allocate slices when size is known
- Use
cm.ToList()
efficiently
Best Practices
Do's
- ✅ Use Result types for error handling
- ✅ Validate input early and clearly
- ✅ Keep functions small and focused
- ✅ Use meaningful error messages
- ✅ Test with wasmtime locally
- ✅ Use
go fmt
before committing
Don'ts
- ❌ Don't use goroutines (not supported in TinyGo WASM)
- ❌ Don't use packages requiring cgo
- ❌ Don't panic in production code
- ❌ Don't ignore error returns
- ❌ Don't use reflection heavily
Security
- Validate all inputs
- Use minimal permissions in noorle.yaml
- Never log sensitive data
- Sanitize error messages
- Handle timeouts appropriately
Advanced Topics
Custom WASI Interfaces
wit
// Advanced WIT with interfaces
interface types {
record config {
endpoint: string,
timeout: u32,
retries: u8,
}
record result {
data: list<u8>,
status: u16,
}
}
world my-plugin {
use types.{config, result};
export process-with-config: func(cfg: config) -> result;
}
go
// Using generated types
import "github.com/example/my-plugin/gen/types"
func (c Component) ProcessWithConfig(cfg types.Config) types.Result {
// Use the config
return types.Result{
Data: processedData,
Status: 200,
}
}
Performance Monitoring
go
import "time"
func (c Component) TimedProcess(input string) cm.Result[string, string] {
start := time.Now()
defer func() {
// Log timing (will show in wasmtime stderr)
println("Processing took:", time.Since(start))
}()
return c.Process(input)
}
Working with Custom Types
go
// Complex types from WIT
import "github.com/example/my-plugin/gen/types"
func (c Component) HandleComplex(req types.Request) cm.Result[types.Response, string] {
// Validate request
if req.Id == "" {
return cm.Err[types.Response, string]("Request ID required")
}
// Process
response := types.Response{
Id: req.Id,
Status: types.StatusSuccess,
Data: processRequest(req),
}
return cm.OK[types.Response, string](response)
}
Resources
Documentation
Tools
- TinyGo - Go compiler for WASM
- wit-bindgen-go - Generate Go bindings
- wasm-tools - WASM utilities
Examples
Next Steps
Continue Learning
- Other Language Guides - Explore Python, TypeScript, JavaScript, Go, Rust
- Configuration - Advanced plugin settings
- Publishing - Deploy your plugin
Build Your Plugin
- Quick Start - Step-by-step guide
- Project Structure - Understanding plugin anatomy
- Platform Overview - How plugins work in Noorle