workers-go banner

workers-go

workers-go is fork of syumai's workers ❀️ β€” a lightweight package for building and running Go on Cloudflare Workers using WebAssembly. Also compatible with NodeJS, Bun, and Deno

πŸ“œ docs ⭐ main repository πŸͺž mirror repository

GitHub is a mirror, all development is centered on codeberg.org. Issues are welcomed on both

Getting Started

This library has only been tested on Go 1.24 with NodeJS 24, but it should be compatible with newer versions

# minimal worker with only GET /hello
pnpm create cloudflare@latest --template=github.com/darckfast/workers-go/_apps/_minimal_worker

# tidy modules
go mod tidy

# dev
pnpx wrangler dev

# deploy
pnpx wrangler deploy

Requirements

  • Go 1.24
  • TinyGo (required to use the -tiny build arg)
  • NodeJS 24

NOTE: Newer Golang versions have improved performance, but it also increases significantly the binary size, and for this reason this project is made using Go 1.24

CLI

This library uses its own cli to create the entrypoint file, bindings, compile, and compress your binary

It’s already included in all workers templates, but it can also be installed manually

# install
go install codeberg.org/darckfast/workers-go/cmd/workers-go@latest

# using
workers-go -h

#Usage of workers-go:
#  -ex value
#        Include a exports * from "directory" - the directory must contain a index.js(ts) file with the desired exports
#  -i string
#        Root directory of your Go worker
#  -o string
#        Output directory (default "./bin")
#  -s    Hide info logs
#  -tiny
#        Use tinygo to compile the project

We use wasm-opt as an extra step of compression, this step is skipped if this is not available

Making HTTP Requests

r, _ := http.NewRequest("GET", "https://google.com", nil)
c := http.Client{
    Timeout: 5 * time.Second,
    Transport: fetch.DefaultCFTransport,
}

// Timeouts return an error
rs, err := c.Do(r)

if err != nil {
    // do error handling
}

defer rs.Body.Close()
b, _ := io.ReadAll(rs.Body)

fmt.Println(string(b))

Cloudflare Bindings

This section contains documentation and examples for running this package within Cloudflare Workers platform, also binding for Cloudflare services like D1, R2, Cache API and more

Handlers

Each handler implemented in Go will make a specific callback available in the JavaScript’s globalThis

GoJavaScript
fetch.ServeNonBlock()cf.fetch()
cron.ScheduleTaskNonBlock()cf.scheduled()
queues.ConsumeNonBlock()cf.queue()
tail.ConsumeNonBlock()cf.tail()
email.ConsumeNonBlock()cf.email()
rpc.RPCStubCreates the necessary stub in the entrypoint file

Below are codes snippets showing how to implement each handler in Go

RPC stubs

The snippets below shows how to create .echo() stub in a worker. This function only accepts []UInt8Array and returns []UInt8Array, and in order to send or receive data, you can usenew TextEncoder().encode() and new TextDecoder().decode() respectively.

//go:build js && wasm

package main

import "codeberg.org/darckfast/workers-go/platform/cloudflare/rpc"

func main() {
	/*
	 * RPCStub must be called to instantiate the RPC function, this function
	 * will be initialized in your entrypoint class
	 */
	rpc.RPCStub("echo", func(c context.Context, args [][]byte) [][]byte {
        // your logic
		return [][]byte{}
	})

    <-make(chan struct{}) // required
}

Using this stub in JavaScript

let dataArray = await worker.echo(new TextEncoder().encode("my data"))

for (let i = 0; i < dataArray.length; i++) {
    console.log(new TextDecoder().decode(dataArray[i]))
}

fetch()

You can use any router on ServeNonBlock(), and for simple services and operations, it’s recommend sticking to either net/http or httprouter. As for why check the binary size comparison for more details

//go:build js && wasm

package main

import "codeberg.org/darckfast/workers-go/platform/cloudflare/fetch"

func main() {
    http.HandleFunc("/hello", func (w http.ResponseWriter, req *http.Request) {
        //...
    })

    fetch.ServeNonBlock(nil)// if nil is given, http.DefaultServeMux is used.

    <-make(chan struct{}) // required
}

scheduled()

//go:build js && wasm

package main

import "codeberg.org/darckfast/workers-go/platform/cloudflare/scheduled"


func main() {
    cron.ScheduleTaskNonBlock(func(ctx context.Context, event *cron.CronEvent) error {
        // ... my scheduled task
        return nil
    })

    <-make(chan struct{})// required
}

queue()

//go:build js && wasm

package main

import 	"codeberg.org/darckfast/workers-go/platform/cloudflare/queues"

func main() {
    queues.ConsumeNonBlock(func(ctx context.Context, batch *queues.MessageBatch) error {
        for _, msg := range batch.Messages {
            // ... process the message
            msg.Ack()
        }

        return nil
    })

    <-make(chan struct{})// required
}

tail()

//go:build js && wasm

package main

import "codeberg.org/darckfast/workers-go/platform/cloudflare/tail"


func main() {
    tail.ConsumeNonBlock(func(ctx context.Context, f *[]tail.TailItem) error {
        // ... process tail trace events
        return nil
    })

    <-make(chan struct{})// required
}

email()

//go:build js && wasm

package main

import "codeberg.org/darckfast/workers-go/platform/cloudflare/email"


func main() {
    email.ConsumeNonBlock(func(ctx context.Context, f *email.ForwardableEmailMessage) error {
        // ... process the email
        return nil
    })

    <-make(chan struct{})// required
}

Envs

All Cloudflare Worker’s envs are copied into Golang os.Environ(), making them available at runtime through os.Getenv(). Only string values are copied

KV

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/kv"
)

func main() {
    // NewNamespace is the first step to interact with KV
    // it gets the KV binding from the js runtime
    kvStore, _ := kv.NewNamespace("TEST_NAMESPACE") 
    
    keyList, _ := kvStore.List(nil)
    
    // Get always expect a list of keys
    // and returns a map[string]string using the "text" option
    values, _ := kvStore.Get([]string{"key1", "key2"}, 0)

    valuesWithMetadata, _ := kvStore.GetWithMetadata(key, 0)

    // this uses the "stream" option
    // and returns a ReadCloser
    valueAsReader, _ := kvStore.GetAsReader(key, 0)


    kvStore.Delete(key)

    // All values with be stringified using
    // before being saved
    kvStore.Put("key", "value as string", &kv.PutOptions{
        Metadata: map[string]any{
            "ver": "0.0.1",
        },
    })

    // uses a readablestream on put
    kvStore.PutReader("key", /* ReadCloser */ , nil)
}

D1

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/v2/d1"
)

func main() {
    db, _ := d1.GetDB("DB_BINDING_NAME")

    // also supports First() and Raw()
	result, err := db.Prepare("SELECT id, data, created_at, updated_at FROM Testing WHERE id = ?").
		Bind(1).
		Run()

	stmt1 := db.Prepare("INSERT INTO Testing (data) VALUES (?)").Bind("entry 1")
	stmt2 := db.Prepare("INSERT INTO Testing (data) VALUES (?)").Bind("entry 2")

	batchResult, err := db.Batch([]d1.D1PreparedStatment{*stmt1, *stmt2})

    execResult, err := db.Exec("UPDATE Testing SET data = null WHERE id = 1")

    session := db.WithSession("MY_SESSION")
}

NOTE: It’s used JSON to transfer data between the JavaScript runtime and Golang, BLOBs are not supported

Cache API

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/cache"
)

func main() {
    c := cache.New()

    res, _ := c.Match(r, nil)

    if res == nil {
        // the response must contain a http.header cache control
        _ = c.Put(r, res) 
    }
}

R2

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/r2"
)

func main() {
    bucket, _ := r2.NewBucket("R2_BINDING_NAME")

    // Put only accepts ReadCloser, you can use the io.NopCloser(bytes.NewReader(data))
    // to create a nop reader
	result, err := bucket.Put("count", reader, 1024, nil)

    data, err := bucket.Get("count")
    list, err := bucket.List()
    err := bucket.Delete("key")
    head, err := bucket.Head("count")
}

Durable objects

Creating Durable Objects

When creating Durable Objects from Go code, you must create them in a main package as each DO will load their own wasm binary at runtime. The workers-go cli will create all the necessary bindings and translating necessary prior to compilation, the output will be available at bin/durable_objects/ directory.

//go:build js && wasm

package main

import (
	"context"
	"net/http"
	"strconv"
	"time"

	"codeberg.org/darckfast/workers-go/platform/cloudflare/durableobjects"
)

// Durable Object to be exported
type MyDurableObject struct {
	durableobjects.DurableObject // Required embed
	InitAt                       int64
}

// type Greeting = { Time: number, Custom: { ID: string, Name: string, Time: number, On: boolean, Ratio: number } }
type Greeting struct {
	Time   int64
	Custom struct {
		ID    string
		Name  string
		Time  int64
		On    bool
		Ratio float64
	}
}

// async SayHello(g: Greeting): Promise<string>
func (d *MyDurableObject) WhatTimsIsIt(_ context.Context, g *Greeting) Greeting {
	return Greeting{Time: time.Now().UnixMilli()}
}

// async Goodbye(g: Greeting, a: number): Promise<[string, number]>
func (d *MyDurableObject) Goodbye(_ context.Context, g *Greeting, a *int64) (string, int64) {
	return "Goodbye", *a
}

// async SayHello(g: Greeting): Promise<string>
func (d *MyDurableObject) SayHello(_ context.Context, g *Greeting) string {
	return "Hello world, " + strconv.FormatInt(d.InitAt, 10)
}

// async fetch(r: Request) Promise<Response>
func (d *MyDurableObject) fetch(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello from fetch request"))
}

// called once inside the constructor()
func (d *MyDurableObject) init(_ context.Context) {
	d.InitAt = time.Now().UnixMicro()
}

Calling Durable Objects stubs

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/durableobjects"
)

func main() {
	n, _ := durableobjects.NewDurableObjectNamespace("DURABLE_OBJECT_BINDING")
	objId := n.IdFromName("my-id")
	stub, _ := n.Get(objId)

	result, err := stub.Call("sayHello")
}

Containers

//go:build js && wasm

package main

import (
	"codeberg.org/darckfast/workers-go/platform/cloudflare/durableobjects"
)

func main() {
	c, err := durableobjects.GetContainer("GO_CONTAINER", "test")

	rs, _ := c.ContainerFetch(r)
}

NOTE: Containers on Cloudflare still in Beta, and their API still not solidified, therefore only the containerFetch has been implemented so far

⚠️ Caveats

C Binding

If you use any library or package that depends on or use any C bindings, your worker will either not compile or panic at runtime. See for more details https://github.com/golang/go/issues/55351

Some examples

PackageCompatible
https://github.com/anthonynsimon/bildβœ…
https://github.com/nfnt/resizeβœ…
https://github.com/bamiaux/rezβœ…
https://github.com/kolesa-team/go-webp❌
https://github.com/Kagami/go-avif❌
https://github.com/h2non/bimg❌
https://github.com/davidbyttow/govips❌
https://github.com/gographics/imagick❌

Not all WASM package will work within as specific runtime, for example https://github.com/kleisauke/wasm-vips is a WASM wrapper around lib https://github.com/libvips/libvips, a image processing library made in C, wasm-vips works within NodeJS, Done and Browser runtime but it tries to access Services Workers, which is unavailable in some edge runtimes like workerd (Cloudflare)

Errors

Although we can wrap JavaScript errors in Go, at the moment there is no source maps available in wasm, meaning we can get errors messages, but not a useful stack trace

Build constraint

For gopls to show syscall/js method’s signature and auto complete, either export GOOS=js && export GOARCH=wasm or add the comment //go:build js && wasm at the top of your Go files

Binary size

Compiling the below sample of code, using each respective router

package main

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

func main() {
        mux := http.NewServeMux()

        mux.HandleFunc("POST /echo", func(w http.ResponseWriter, r *http.Request) {
                w.Header().Set("content-type", "application/json")
                var payload map[string]any
                defer r.Body.Close()
                json.NewDecoder(r.Body).Decode(&payload)
                payload["received"] = true
                json.NewEncoder(w).Encode(payload)
        })

        http.ListenAndServe(":1234", mux)
}

The binary was compiled using the following command, on Go 1.24

GOOS=js GOARCH=wasm go build -o routername
RouterSize
httprouter9.96Mb
net/http9.98Mb
gorilla10.45Mb
chi10.46Mb
echo11.01Mb
fiber15.60Mb

This is issue is also relevant for this https://github.com/golang/go/issues/6853

Benchmarks

We used this https://github.com/darckfast/workers-go-benchmark to collect the results bellow.

Avg Response Time (ms)
Vanilla     β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                           β”‚  4.81ms
workers-rs  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                        β”‚  6.04ms
workers-go  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚ 14.25ms

p95 Response Time (ms)
Vanilla     β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                                 β”‚  8.25ms
workers-rs  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                         β”‚ 17.13ms
workers-go  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚ 41.74ms

Throughput (req/s)
workers-go  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚  9.84 req/s
Vanilla     β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚  9.94 req/s
workers-rs  β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚  9.93 req/s
RuntimeAvg (ms)p90 (ms)p95 (ms)Max (ms)Req/s
workers-go14.2529.5441.7452.279.84
workers-rs6.048.9617.1319.749.93
Vanilla4.817.578.2518.029.94