Skip to content

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

FilePurpose
main.goPlugin implementation with exported functions
wit/world.witWebAssembly interface definition
go.modGo module dependencies
wkg.tomlWIT dependency configuration
build.shCompilation and component creation
prepare.shInstalls 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:

  1. Generates Go bindings from WIT
  2. Compiles with TinyGo to wasm32-wasip2
  3. wkg fetches WIT dependencies from imports
  4. Creates component with wasm-tools
  5. Optimizes output
  6. 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

Examples

Next Steps

Continue Learning

Build Your Plugin