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
Innoorle.yaml:
permissions:
filesystem:
allow:
- uri: "fs://cache/**"
access: [read, write]
- uri: "fs://config/**"
access: [read]
environment:
allow:
- key: "CACHE_DIR"
Environment Variables
Access configuration viaos.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"),
}
}
.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
}
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()
}
}
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
- Remove unnecessary dependencies
- Use
go mod tidyto clean up - Enable
-s -win LDFLAGS
- Ensure go.mod is correct
- Use
go getto fetch versions - Rebuild after dependency changes