urban dictionary banner

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

πŸ“œ workers-go repository

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 implementationJS 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
RouterSize in bytes
httprouter9967981
net/http9986162
gorilla10451195
chi10463569
echo11019095
fiber15601544

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
}

email

//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

FeatureImplementedNotes
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

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❌

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