TinyGo SDK

0.4 NEW — пишем балансер на Go, компилируем в WASM через TinyGo.

Установка TinyGo

brew tap tinygo-org/tools
brew install tinygo
tinygo version       # 0.41+ нужен для wasi-таргета с reactor mode

Минимальный плагин (30 строк)

wasm_modules/hello/main.go:

package main

import (
    "encoding/json"
    "lampac.cc/sdk/lampac"
)

func main() {} // обязателен для TinyGo wasi target

type item struct{ Name string `json:"name"` }
type movie struct {
    Type string `json:"type"`
    Data []item `json:"data"`
}

//export handle
func handle(ptr, length uint32) uint64 {
    inv := lampac.ReadInvocation(ptr, length)
    lampac.Info("got request, path=" + inv.Path)
    body, _ := json.Marshal(movie{
        Type: "movie",
        Data: []item{{Name: "Hello, " + inv.Query["q"]}},
    })
    return lampac.WriteResponse(body)
}

wasm_modules/hello/manifest.json:

{
  "id": "hello",
  "name": "Hello plugin",
  "version": "0.1.0",
  "target": "server",
  "language": "tinygo",
  "abi_version": 1,
  "permissions": []
}

wasm_modules/hello/go.mod:

module lampac.cc/wasm-modules/hello

go 1.22

require lampac.cc/sdk/lampac v0.0.0

replace lampac.cc/sdk/lampac => ../../wasm_sdk/tinygo

wasm_modules/hello/Makefile:

.PHONY: build
build:
	tinygo build -o plugin.wasm -target wasi -no-debug ./

Сборка и тест:

make
curl 'http://localhost:9118/lite/hello?q=world'
# {"type":"movie","data":[{"name":"Hello, world"}]}

Никакого рестарта lampac-go — fsnotify подхватил plugin.wasm автоматически.

Что есть в SDK

wasm_sdk/tinygo/lampac.go экспортирует:

Лог

lampac.Debug("…")
lampac.Info("…")
lampac.Warn("…")
lampac.Error("…")

HTTP

resp, err := lampac.HTTPGet(lampac.HTTPRequest{
    URL:     "https://api.example.com/v1/movies?id=123",
    Headers: map[string]string{"User-Agent": "lampac"},
})
// resp.Status, resp.Headers, resp.Body, resp.Err

resp, err := lampac.HTTPPost(lampac.HTTPRequest{
    URL:  "https://api.example.com/auth",
    Body: `{"login":"x","pass":"y"}`,
    Headers: map[string]string{"Content-Type": "application/json"},
})

resp.Body — уже декодированные байты (хост возвращает base64, SDK раскодирует).

Proxy URL

url := lampac.ProxyURL("https://cdn.example.com/movie.m3u8", lampac.ProxyOpts{
    Plugin:  "hello",                              // опционально
    Headers: map[string]string{"Origin": "..."},   // headers для upstream
})
// url == "https://lampac-host/proxy/<aes-blob>"

Кеш

lampac.CacheSet("user:42", []byte(`{"name":"alice"}`), 60) // ttl 60 сек
val, ok := lampac.CacheGet("user:42")                       // []byte, bool

Invocation

inv := lampac.ReadInvocation(ptr, length)
// inv.Query, inv.Headers, inv.Host, inv.RequestIP,
// inv.Path, inv.Life, inv.Checksearch, inv.UserAgent, inv.Config

var cfg struct {
    APIHost string `json:"apiHost"`
    Token   string `json:"token"`
}
_ = json.Unmarshal(inv.Config, &cfg)

Ответ

return lampac.WriteResponse(jsonBytes)         // плагин владеет lifecycle памяти

Полный пример с HTTP + кешом

package main

import (
    "encoding/json"
    "fmt"

    "lampac.cc/sdk/lampac"
)

func main() {}

type cfg struct {
    APIHost string `json:"apiHost"`
}

//export handle
func handle(ptr, length uint32) uint64 {
    inv := lampac.ReadInvocation(ptr, length)

    var c cfg
    _ = json.Unmarshal(inv.Config, &c)

    id := inv.Query["id"]
    if id == "" {
        return lampac.WriteResponse([]byte(`{"data":[]}`))
    }

    cacheKey := "movie:" + id
    if v, ok := lampac.CacheGet(cacheKey); ok {
        return lampac.WriteResponse(v) // hit — отдаём без HTTP
    }

    resp, err := lampac.HTTPGet(lampac.HTTPRequest{
        URL: fmt.Sprintf("%s/api/movies/%s", c.APIHost, id),
    })
    if err != nil || resp.Status != 200 {
        return lampac.WriteResponse([]byte(`{"data":[],"rch":false}`))
    }

    lampac.CacheSet(cacheKey, resp.Body, 600)
    return lampac.WriteResponse(resp.Body)
}

Размер бинарника

Что собрано TinyGo (-no-debug)
Echo (демо со всеми host-импортами) ~410 КБ
Quality filter (middleware) ~420 КБ
Welcome toast (только client API, без net/json) ~24 КБ

tinygo build -no-debug -opt=z (по умолчанию -opt=2) сократит ещё на 10–15%.

Подводные камни

  1. Не используй os.Exit / panic — модуль закроется, до следующего запроса всё равно создастся новый, но крэш-дамп фиксируется.
  2. JSON через encoding/json тянет ~200 КБ — если плагин маленький и тебе хватит ручной сборки строк, лучше так.
  3. Гонки: одна wazero-инстанция на модуль обслуживает все запросы под sync.Mutex. Если плагин CPU-тяжёлый, пишите так, чтобы handle() возвращался быстро.
  4. unsafe.Pointer(&b[0]) на пустом срезе паникует — SDK уже это учитывает, но если вы делаете свои imports вручную, не забудьте.

Что ещё посмотреть