
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
-tinybuild 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
| Go | JavaScript |
|---|---|
fetch.ServeNonBlock() | cf.fetch() |
cron.ScheduleTaskNonBlock() | cf.scheduled() |
queues.ConsumeNonBlock() | cf.queue() |
tail.ConsumeNonBlock() | cf.tail() |
email.ConsumeNonBlock() | cf.email() |
rpc.RPCStub | Creates 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
containerFetchhas 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
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 | Router | Size |
|---|---|
| httprouter | 9.96Mb |
| net/http | 9.98Mb |
| gorilla | 10.45Mb |
| chi | 10.46Mb |
| echo | 11.01Mb |
| fiber | 15.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 | Runtime | Avg (ms) | p90 (ms) | p95 (ms) | Max (ms) | Req/s |
|---|---|---|---|---|---|
workers-go | 14.25 | 29.54 | 41.74 | 52.27 | 9.84 |
workers-rs | 6.04 | 8.96 | 17.13 | 19.74 | 9.93 |
| Vanilla | 4.81 | 7.57 | 8.25 | 18.02 | 9.94 |