Skip to main content
Build WebAssembly plugins using Go. Noorle uses TinyGo to compile Go to WASI Preview 2 WebAssembly.

Setup

Install TinyGo

# Install TinyGo (required for Go plugins)
wget https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo_0.30.0_amd64.deb
sudo dpkg -i tinygo_0.30.0_amd64.deb

# Or on macOS
brew install tinygo

# Create a new Go plugin
noorle plugin new my-go-plugin --lang go
cd my-go-plugin

Project Structure

my-go-plugin/
├── main.go               # Entry point and tools
├── tools/
│   └── process.go        # Tool implementations
├── go.mod                # Module definition
├── go.sum                # Dependencies
├── noorle.yaml           # Plugin config
├── world.wit             # Component interface
├── .env
└── Makefile

Define Tools

Export functions as plugin tools.

Simple Functions

// main.go
package main

// Greet returns a greeting
func Greet(name string) string {
    return "Hello, " + name + "!"
}

// Add returns the sum of two numbers
func Add(a, b float64) float64 {
    return a + b
}

// Multiply returns the product
func Multiply(a, b float64) float64 {
    return a * b
}

Complex Types

package main

type Item struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Count int    `json:"count"`
}

type Config map[string]string

type Result struct {
    Success bool   `json:"success"`
    Data    string `json:"data"`
    Error   string `json:"error,omitempty"`
}

// ProcessItems transforms items with config
func ProcessItems(items []Item, config Config) []Item {
    result := make([]Item, len(items))
    prefix, ok := config["prefix"]
    if !ok {
        prefix = ""
    }

    for i, item := range items {
        result[i] = Item{
            ID:    item.ID,
            Name:  prefix + item.Name,
            Count: item.Count,
        }
    }
    return result
}

// SearchItems returns matching items
func SearchItems(query string, limit int) []Item {
    if limit <= 0 {
        limit = 10
    }
    if limit > 100 {
        limit = 100
    }

    result := make([]Item, limit)
    for i := 0; i < limit; i++ {
        result[i] = Item{
            ID:    string(rune(i)),
            Name:  query + "_" + string(rune(i)),
            Count: i + 1,
        }
    }
    return result
}

File I/O

Access filesystem via standard library.
package main

import (
    "os"
    "path/filepath"
)

func ReadConfig() (string, error) {
    data, err := os.ReadFile("/allowed/path/config.json")
    if err != nil {
        return "", err
    }
    return string(data), nil
}

func WriteCache(key, value string) (bool, error) {
    cacheDir := os.Getenv("CACHE_DIR")
    if cacheDir == "" {
        cacheDir = "/cache"
    }

    filePath := filepath.Join(cacheDir, key+".txt")
    err := os.WriteFile(filePath, []byte(value), 0644)
    return err == nil, err
}

func ListFiles(directory string) ([]string, error) {
    entries, err := os.ReadDir(directory)
    if err != nil {
        return nil, err
    }

    var files []string
    for _, entry := range entries {
        if !entry.IsDir() {
            files = append(files, entry.Name())
        }
    }
    return files, nil
}

Configure Permissions

In noorle.yaml:
permissions:
  filesystem:
    allow:
      - uri: "fs://cache/**"
        access: [read, write]
      - uri: "fs://config/**"
        access: [read]

  environment:
    allow:
      - key: "CACHE_DIR"

Environment Variables

Access configuration via os.Getenv().
package main

func GetApiCredentials() map[string]string {
    return map[string]string{
        "api_key":    os.Getenv("API_KEY"),
        "api_secret": os.Getenv("API_SECRET"),
    }
}

func GetDatabaseUrl() string {
    url := os.Getenv("DATABASE_URL")
    if url == "" {
        return "sqlite:///:memory:"
    }
    return url
}

func GetConfig() map[string]string {
    return map[string]string{
        "debug":      os.Getenv("DEBUG"),
        "log_level":  os.Getenv("LOG_LEVEL"),
        "timeout":    os.Getenv("TIMEOUT"),
    }
}
Configure in .env:
API_KEY=sk-1234567890
API_SECRET=secret-value
DATABASE_URL=postgres://localhost/mydb
DEBUG=true
LOG_LEVEL=debug

Network Access

Make HTTP requests via standard library.
package main

import (
    "io"
    "net/http"
)

func FetchData(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    return string(data), err
}

func PostJSON(url string, jsonData string) (string, error) {
    resp, err := http.Post(
        url,
        "application/json",
        strings.NewReader(jsonData),
    )
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    return string(data), err
}

func CallApi(endpoint string, params map[string]string) (string, error) {
    url := "https://api.example.com/" + endpoint + "?"
    first := true
    for k, v := range params {
        if !first {
            url += "&"
        }
        url += k + "=" + url.QueryEscape(v)
        first = false
    }

    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    return string(data), err
}
Configure network permissions:
permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "*.github.com"

Error Handling

Return errors as structured responses.
package main

type Response struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

func SafeOperation(input string) Response {
    if input == "" {
        return Response{
            Success: false,
            Error:   "Empty input",
        }
    }

    result, err := process(input)
    if err != nil {
        return Response{
            Success: false,
            Error:   err.Error(),
        }
    }

    return Response{
        Success: true,
        Data:    result,
    }
}

func process(input string) (string, error) {
    if input == "" {
        return "", errors.New("invalid input")
    }
    return strings.ToUpper(input), nil
}

Dependencies

go.mod

module my-go-plugin

go 1.21

require (
    github.com/google/uuid v1.5.0
)

Add Dependencies

go get github.com/google/uuid
noorle plugin build

Size Optimization

Go WASM is typically 1-3MB. Minimize with:
# Build with optimizations
GOOS=js GOARCH=wasm go build -ldflags="-s -w"

Testing

Test locally before uploading.
// main_test.go
package main

import (
    "testing"
)

func TestGreet(t *testing.T) {
    result := Greet("Alice")
    expected := "Hello, Alice!"
    if result != expected {
        t.Errorf("Expected %q, got %q", expected, result)
    }
}

func TestAdd(t *testing.T) {
    if Add(2, 3) != 5 {
        t.Fail()
    }
}
Run tests:
go test ./...

Building and Deploying

Build

noorle plugin build

# Output:
# ✓ Building plugin...
# ✓ Compiling Go to WebAssembly
# ✓ Creating my-go-plugin.npack (1.2MB)

Upload

noorle plugin push --api-key YOUR_KEY

# Output:
# ✓ Uploading plugin...
# ✓ Plugin ID: 550e8400-e29b-41d4-a716-446655440000
# ✓ Active version: v1
# ✓ Tools: Greet, Add, ProcessItems

Best Practices

Input Validation

func ValidateEmail(email string) map[string]interface{} {
    errors := []string{}

    if email == "" {
        errors = append(errors, "Email cannot be empty")
    }

    if !strings.Contains(email, "@") {
        errors = append(errors, "Email must contain @")
    }

    if len(email) > 255 {
        errors = append(errors, "Email too long")
    }

    if len(errors) > 0 {
        return map[string]interface{}{
            "valid":  false,
            "errors": errors,
        }
    }

    return map[string]interface{}{
        "valid": true,
    }
}

Logging

import "log"

func LogInfo(msg string) {
    log.Printf("[INFO] %s\n", msg)
}

func MyTool(input string) string {
    LogInfo("Processing: " + input)
    result := process(input)
    LogInfo("Completed")
    return result
}

Documentation

// ProcessWeatherData processes raw weather data
// and returns formatted results.
//
// city: The city name
// units: Temperature units (celsius or fahrenheit)
//
// Returns: Weather summary or error
func GetWeather(city, units string) (string, error) {
    // Implementation
}

Example: Complete Plugin

// main.go
package main

import (
    "encoding/json"
    "io"
    "net/http"
    "os"
)

type Weather struct {
    City        string `json:"city"`
    Temp        float64 `json:"temp"`
    Description string `json:"description"`
}

func GetWeather(city, units string) (Weather, error) {
    apiKey := os.Getenv("OPENWEATHER_API_KEY")
    if apiKey == "" {
        return Weather{}, errors.New("API key not configured")
    }

    url := "https://api.openweathermap.org/data/2.5/weather?q=" +
        city + "&appid=" + apiKey + "&units=" + units

    resp, err := http.Get(url)
    if err != nil {
        return Weather{}, err
    }
    defer resp.Body.Close()

    var data map[string]interface{}
    body, _ := io.ReadAll(resp.Body)
    json.Unmarshal(body, &data)

    main := data["main"].(map[string]interface{})
    weather := data["weather"].([]interface{})[0].(map[string]interface{})

    return Weather{
        City:        data["name"].(string),
        Temp:        main["temp"].(float64),
        Description: weather["description"].(string),
    }, nil
}

Troubleshooting

Build fails - “TinyGo not found”:
# Install TinyGo
brew install tinygo
# or from source
git clone https://github.com/tinygo-org/tinygo
cd tinygo && make
Plugin too large (>5MB):
  • Remove unnecessary dependencies
  • Use go mod tidy to clean up
  • Enable -s -w in LDFLAGS
Runtime issues with dependencies:
  • Ensure go.mod is correct
  • Use go get to fetch versions
  • Rebuild after dependency changes

Next Steps