
workers-go
workers-go is a fork of syumai's workers β€οΈ depedency free Go library, made to help interface Go's WASM with Cloudflare Workers.
Powered by Vite and
Cloudflare Wrangler
Install
This library has only been tested on Go 1.23+ with NodeJS 22+
go get github.com/Darckfast/workers-go
Handlers
Use all available Cloudflare Wokers handlers, plus other services like KV, D1, R2, Durable Objects, Containers, Sockets, Cache API, Queue producer and more
Each handler implemented in Go will make a specific callback available in the JavaScriptβs global scope
Go implementation | JS callback |
---|---|
fetch.ServeNonBlock() | cf.fetch() |
cron.ScheduleTaskNonBlock() | cf.scheduled() |
queues.ConsumeNonBlock() | cf.queue() |
tail.ConsumeNonBlock() | cf.tail() |
email.ConsumeNonBlock() | cf.email() |
Below are codes snippets showing how to implement each handler in Go
fetch
//go:build js && wasm
package main
import "github.com/Darckfast/workers-go/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
}
Although you can use any router on ServeNonBlock()
, for simple services and operations,
i would recommend sticking to either net/http
or httprouter
since this is WASM and Edge runtimes, and binary size do matter. As for why, below is a
very simple benchmark, comparing the binary size yielded by using a few popular
routers
Binary size benchmark
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
GOOS=js GOARCH=wasm go build -o routername
Router | Size in bytes |
---|---|
httprouter | 9967981 |
net/http | 9986162 |
gorilla | 10451195 |
chi | 10463569 |
echo | 11019095 |
fiber | 15601544 |
scheduled
//go:build js && wasm
package main
import "github.com/Darckfast/workers-go/cloudflare/cron"
func main() {
cron.ScheduleTaskNonBlock(func(event *cron.CronEvent) error {
// ... my scheduled task
return nil
})
<-make(chan struct{})// required
}
queue
//go:build js && wasm
package main
import "github.com/Darckfast/workers-go/cloudflare/queues"
func main() {
queues.ConsumeNonBlock(func(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 "github.com/Darckfast/workers-go/cloudflare/tail"
func main() {
tail.ConsumeNonBlock(func(f *[]tail.TailItem) error {
// ... process tail trace events
return nil
})
<-make(chan struct{})// required
}
//go:build js && wasm
package main
import "github.com/Darckfast/workers-go/cloudflare/email"
func main() {
email.ConsumeNonBlock(func(f *email.ForwardableEmailMessage) error {
// ... process the email
return nil
})
<-make(chan struct{})// required
}
Making HTTP Requests
For compatibility reasons, you MUST use the fetch.Client{}
to make http request, as it interfaces Goβs http with Cloudflare Worker fetch()
API.
r, _ := http.NewRequest("GET", "https://google.com", nil)
c := fetch.Client{
Timeout: 5 * time.Second,
}
// Timeouts return error
rs, err := c.Do(r)
defer rs.Body.Close()
b, _ := io.ReadAll(rs.Body)
fmt.Println(string(b))
β οΈβ οΈ Although you can convert fetch.Client
to http.Client
be aware that by using the http.Client{}.Do()
more std dependencies will be included to handle the https request, and it will increase significantly the final binary
going from 5.6MB up to 11MB (1.6MB to 2.8MB gzipped)
Project structure
Here is a sample of a worker structure
βββ bin # contains the compiled wasm binary + wasm_exec.js
β βββ app.wasm
β βββ wasm_exec.js
βββ biome.json # formatter and linter
βββ main.ts # main worker's entrypoint
βββ migrations # d1 migrations
β βββ 0001_add_aimple_table.sql
βββ package.json
βββ pkg # folder for your Go's code
β βββ main.go
βββ tsconfig.json
βββ types # default typescript types
β βββ global.d.ts
β βββ go.d.ts
β βββ wasm.d.ts
β βββ worker-configuration.d.ts
βββ vite.config.ts
βββ wrangler.toml
Worker main entrypoint
The main.ts
is the entrypoint, declared in the wrangler.toml
, and its where the wasm binary
will be loaded and used
Below is a non functional example, for a functional and complete example check ./worker/main.ts
import "github.com/Darckfast/workers-go/cloudflare/fetch"
import app from "./bin/app.wasm"; // Compiled wasm binary
import "./bin/wasm_exec.js"; // cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .
/**
* This function is what initialize your Go's compiled WASM binary
* only after this function has finished, that the handlers will be
* defined in the globalThis scope
*
* At the moment, due limitations with the getRandomData(), this block
* cannot be executed at top level, it must be contained within the handlers
* scope
*
* It's REQUIRED and it must be called before using the cf.<handler>()
*/
function init() {
const go = new Go()
/*
* This will execute the binary, and all Go's `init()` will run and instantiate
* the callbacks. They all will be within the globalThis.cf (or just cf) object
*/
go.run(new WebAssembly.Instance(app, go.importObject))
}
async function fetch(req: Request, env: Env, ctx: ExecutionContext) {
init()
return await cf.fetch(req, env, ctx);
}
export default {
fetch,
} satisfies ExportedHandler<Env>;
Building and deploying
To build locally, you can run
vite build
This will compile the wasm binary, and flat the JS dependencies into a single file, the out dir is dist
or ./worker/dist
To deploy, just run
wrangler build
The wrangler
CLI will auto detect, build the project and compress the final gzip
file
If this is your first Cloudflare Worker, check their documentation to setup your first worker
To decrease our binary size, we do small tweaks to remove the debug information from the binary by passing some flags to the build command
GOOS=js GOARCH=wasm go build -ldflags='-s -w' -o ./bin/app.wasm ./pkg/main.go
Features
Below is a list of implemented, and not implemented Cloudflare features
Feature | Implemented | Notes |
---|---|---|
fetch | β | All functions uses either http.Request or http.Response |
queue | β | All bindings have been implemented |
email | β | All bindings have been implemented |
scheduled | β | All bindings have been implemented |
tail | β | All bindings have been implemented |
Envs | β | All Cloudflare Workerβs env are copied into os.Environ() , making them available at runtime with os.Getenv() . Only string typed values are copied |
Containers | π΅ | Only the containerFetch() function has been implemented |
R2 | π΅ | Options for R2 methods still not implementd |
D1 | β | All bindings have been implemented. NOTE: at the moment, to transfer data between JS and Go, itβs used JSON, and BLOB are not supported |
KV | π΅ | Options for KV methods still not implemented |
Cache API | β | Implemented all bindings |
Durable Objects | π΅ | Only stub calls have been implemented |
Service binding | β | fetch.Client{}.WithBinding(serviceName) . only works for fetch or HTTP requests |
HTTP | β | native fetch interface using fetch.Client{}.Do(req) |
HTTP Timeout | β | Implemented using the same interface as http.Client{ Timeout: 20 * time.Second } |
HTTP RequestInitCfProperties | β | Implemented all but the image property, they must be set on the http.Client{ CF: &RequestInitCF{} } |
Lifecycle | β | Both ctx and env are exposed to be used in Go through lifecycle.Ctx and lifecycle.Env variables |
TCP Sockets | β | All bindings have been implemented |
Queue producer | β | All bindings have been implemented |
β οΈ Caveats
C Binding
IF you use any library or package that depends or use any C binding, or C compiled code, compiling to WASM is not possible
Some examples
Queues
Cloudflare Queue locally is incredibly slow to produce events (up to 7 seconds)
TinyGo
Goβs compiled binary can exceed the Free 3MB Cloudflare Workerβs limit, in which case one suggestion is to use TinyGo to compile, but for performance reasons workers-go
uses the encoding/json
from the std Goβs library, which makes this package incompatible with the current build of TinyGo
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
Benchmarks
This synthetic benchmark does not mean anything, as both worker and the benchmark ran in the same arm64 machine⦠but here it is
β main.bench.ts > fetch API 64349ms
name hz min max mean p75 p99 p995 p999 rme samples
Β· GET /hello 277.57 1.7850 29.5515 3.6027 3.8909 10.2415 12.7680 25.1109 Β±3.31% 1000
Β· GET /do 271.63 1.7422 19.8682 3.6815 3.6932 13.1721 15.7126 17.6509 Β±3.37% 1000
Β· GET /env 293.63 1.6817 21.8721 3.4056 3.2893 14.8101 16.9406 21.8469 Β±4.15% 1000
Β· POST /kv 91.9708 4.9736 54.5940 10.8730 11.3781 40.4777 43.0436 50.5944 Β±3.39% 1000
Β· GET /kv/list 49.7281 11.2936 95.4239 20.1094 20.2248 71.9358 78.6213 94.5823 Β±3.60% 1000
Β· POST /d1 133.05 4.5911 85.2919 7.5162 7.3831 39.8524 50.7567 65.6853 Β±4.44% 1000
Β· POST /r2 103.39 4.9805 88.1431 9.6718 10.0444 40.5508 54.6049 75.3043 Β±3.81% 1000
Β· POST /application/json 220.64 2.0165 59.6422 4.5323 4.2142 27.3634 49.7361 58.2379 Β±6.96% 1000