workers-go: run a net/http handler on Cloudflare Workers, with the platform bindings generated
Go package to run an HTTP server on Cloudflare Workers.
At a glance
- What is it?
- workers-go is a Go module that compiles an ordinary http.Handler to WebAssembly and serves it from Cloudflare Workers, with bindings for KV, R2, D1, Durable Objects, Queues and Cron auto generated from the Cloudflare type definitions. The README calls it experimental, and the Makefile shows the project is as much a code generation pipeline as a library.
- Who is it for?
- workers-go fits a Go team that wants to keep one codebase and standard net/http semantics while deploying to Cloudflare Workers or Deno Deploy, and that would rather not hand write the platform bindings. It does not fit a team that needs a stable, supported production dependency, because the project describes itself as experimental, and it does not fit a workload that depends on Go behaviour the WebAssembly target does not have.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- syumai/workers-go1,183 ★ · +13
| Repository | 2026-10-21 | 2026-11-07 |
|---|---|---|
| syumai/workers-go | 1.2k | 1.2k |
13 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
One handler, two runtimes, and a WebAssembly boundary
workers-go is a Go module to run an HTTP server written in Go on Cloudflare Workers, and its root package is workers.
The promise is short enough to quote. Implement your http.Handler and give it to workers.Serve().
func main() {
http.HandleFunc("/hello", func (w http.ResponseWriter, req *http.Request) { ... })
workers.Serve(nil) // if nil is given, http.DefaultServeMux is used.
}The nil argument is the detail that makes it a drop-in for existing code. Give it a handler and it uses that, give it nil and it falls back to http.DefaultServeMux, which means a program that already registers routes on the standard mux can be pointed at Workers without restructuring its routing.
The package can easily serve an http.Handler on Cloudflare Workers, and the emphasis on the standard library is the whole point. Nothing in the example above is Cloudflare specific. That is what makes the library interesting, and it is also where the constraints come from, because Go's net/http is being compiled to WebAssembly and run inside a Workers isolate rather than on a normal server.
The repository layout shows the shape of the port. There is handler.go and handler_js.go at the root, with handler_test.go and handler_js_test.go beside them, so the platform specific code is isolated behind a build constraint rather than scattered through the package. Then there is cloudflare/ for the platform bindings, cmd/ for the tool that generates things, internal/ for the internals, and the scripts/ directory that drives generation.
The _js suffix on the two source files is the marker of the approach. The same Go program builds twice, once for the host and once for js and wasm, and the difference between the two is one file.
The bindings are generated, and the Makefile is the real documentation
The documentation list makes the scope clear. There is a Cloudflare page for bindings for KV, R2, D1, Durable Objects, Queues, Cron and more, and a page for generated bindings in exp/cloudflare, which are auto-generated from @cloudflare/workers-types. There is also a Deno and Deno Deploy page for running the same http.Handler via Deno.serve, and an FAQ.
So the design is that the Cloudflare type definitions are the source of truth, and the Go bindings are produced from them. The exp/cloudflare README describes the experimental generated bindings and how they are produced.
The Makefile shows the pipeline in full, and it is the most informative file in the repository.
GO_VERSION ?= 1.27.1
TINYGO_VERSION ?= 0.42.0The gen-wasm-exec target runs a pnpm script under scripts/gen-wasm-exec, passing the Go and TinyGo versions, and the check-generated target runs that generation and then does a git diff against testdata/wasm, cmd/workers-assets-gen/assets, and exits with a difference. That is a generated code check in CI, and it means the committed WebAssembly helper assets cannot drift from what the generator produces.
The bindings have their own pipeline. gen-bindings-extract installs dependencies in scripts/gen-bindings with a frozen lockfile and runs an extract step, gen-bindings then runs a Go program called cfgen against the repository root, and gen-bindings-check runs the same program with a -check flag.
The Deno side has its own generation too, with a gen-deno target and a gen-deno-snapshot target that refreshes a Deno API snapshot file consumed by gen-deno, and it requires the deno binary.
The build-examples target compiles every directory under _examples with GOOS=js GOARCH=wasm, and the README describes _examples as a runnable project for almost every feature. So the repository carries a matrix of runnable examples, one per feature, each built the same way a user would build their own project.
Testing happens in three environments, because the runtime boundary matters
The test targets are separated by environment, and the separation is the point.
The main test target runs the whole suite compiled for JavaScript and WebAssembly, with a special path prepended so that a locally built WebAssembly executable is found first.
test:
@PATH=$(CURDIR)/testdata/wasm:$$PATH GOOS=js GOARCH=wasm go test ./...Then there is test-host, which runs go test on the root package and the cmd directory, the normal host build. And there is test-e2e, which is guarded by a check that the e2e directory exists, and runs with a build tag of e2e, a ten minute timeout and a count of 1, so the end to end tests never reuse a cached result.
test-all runs the three in sequence.
The reason for the split is the nature of the target. A pure Go library would be testable with go test and nothing else. This one has to be verified in the environment it actually runs in, which is a WebAssembly module executing inside a Workers isolate, and a unit test on the host tells you very little about that. The testdata/wasm directory in the repository tree is where the prebuilt WebAssembly executable lives, and the check-generated target verifies it is current.
The e2e suite is separately tagged and separately timed, which is a good sign, because end to end tests against a real Workers runtime are slow and flaky in a way unit tests are not, and mixing them into the default target would make the default target unreliable.
The one thing the repository does not spell out is what the e2e tests actually run against. The e2e directory exists and the tag is e2e, but the README does not say whether that is a local workerd, a deployed worker, or something else, and that is a question worth asking before relying on the suite.
Setting up a project, and what the template does for you
There are two routes to a running project, and the first one is a button.
The README says you can easily create and deploy a project from a Deploy to Cloudflare button, and links to a deploy target that points at a separate template repository. If you want to create a project manually, you follow the guide below.
The requirements are Node.js with npm, and Go 1.24.0 or later.
Creating the project uses the Cloudflare scaffolding tool with a template.
npm create cloudflare@latest -- --template github.com/syumai/workers-go/_templates/cloudflare/worker-goThen you initialise the Go module in the new directory.
cd my-app
go mod init
go mod tidyThe development server is started with npm start, and the quick start also mentions a TinyGo template in the documentation's quickstart section, which is a separate option from the plain Go one.
Verification is a curl against the local server, and the port is the one the Workers local development server uses.
curl http://localhost:8787/helloThe expected response is Hello!, which tells you the whole chain works: the Go handler compiled to WebAssembly, the module loaded, the routing resolved, and the response made it back.
That last step matters more than it sounds. A library that compiles but does not serve is a compile time curiosity, and a single curl that returns the expected body is the cheapest possible end to end check of the runtime boundary. Once you see Hello! you know the deployment shape is right, and everything after that is your own code.
Where the WebAssembly target constrains Go
The honest limitations follow from compiling a Go server to WebAssembly, and it is worth being concrete about them rather than discovering them at deployment time.
The first is that the standard library is not the standard library. The go.mod file is the shortest possible, a module path and a go directive of 1.21.3 and nothing else, which tells you this module pulls in no third party dependencies at all. What it does not tell you is how much of the standard library is actually reachable, and the honest answer is that a Go program built for js and wasm does not have a working filesystem, no threads in the usual sense, and a scheduler that the runtime does not fully support. Code that assumes it can open a file, spawn a goroutine that blocks on real IO, or use cgo will not behave as it does on the host.
The second is that the platform capabilities are not Go's. A KV namespace, an R2 bucket, a D1 database, a Durable Object, a Queue and a Cron trigger are not in the Go standard library, and each one is an object with its own semantics and its own failure modes. The generated bindings exist to bridge that gap, and the generated bindings are also where the sharp edges are, because they follow a TypeScript type definition rather than an idiomatic Go design.
The third is the experimental label, and it should be read literally. The README states in one line that this is an experimental project, and the bindings live under exp/ with their own README describing them as experimental generated bindings. The documentation has a dedicated feature support page listing supported Workers features and their status, which is the page to read before you commit rather than after.
The fourth is the Deno path, which is a genuinely different deployment target rather than a convenience. Deno.serve runs the same handler, but the Deno API is maintained by a separate snapshot generation step that requires the deno binary, and it is refreshed on its own schedule. If your plan is Cloudflare only, ignore it. If your plan is both, you are signing up for two platforms to keep working.
Editorial conclusion
workers-go fits a Go team that wants to keep one codebase and standard net/http semantics while deploying to Cloudflare Workers or Deno Deploy, and that would rather not hand write the platform bindings. It does not fit a team that needs a stable, supported production dependency, because the project describes itself as experimental, and it does not fit a workload that depends on Go behaviour the WebAssembly target does not have. Check three things before you commit. Check the feature support page for the specific Workers feature you need, because the documentation has a page dedicated to supported Workers features and their status, which is where the real limits are listed. Check your Go version and the toolchain, because the requirements call for Go 1.24.0 or later and Node.js, and the module directive in go.mod is 1.21.3, and the Makefile pins a Go version of 1.27.1 and TinyGo 0.42.0 for the generated executable. And check whether the Deno target matters to you, since the same handler can be served through Deno.serve. The last push was on 2026-09-23, the newest release listed is v0.36.0 from 2026-09-21, and the license is MIT.
Frequently asked questions
What is workers-go and how do I use it?
It is a Go module to run an HTTP server written in Go on Cloudflare Workers, with the root package workers. You implement an http.Handler and pass it to workers.Serve(), and passing nil uses http.DefaultServeMux instead.
How do I create a new workers-go project?
Use npm create cloudflare@latest with the template github.com/syumai/workers-go/_templates/cloudflare/worker-go, then run go mod init and go mod tidy in the new directory, npm start to start the development server, and curl http://localhost:8787/hello to verify it returns Hello!. The requirements are Node.js with npm and Go 1.24.0 or later.
Which Cloudflare Workers bindings does workers-go support?
The documentation lists bindings for KV, R2, D1, Durable Objects, Queues, Cron and more, with the generated bindings in exp/cloudflare auto generated from @cloudflare/workers-types and described as experimental.
Can workers-go also run on Deno Deploy?
Yes. The documentation has a Deno and Deno Deploy page for running the same http.Handler via Deno.serve, and the Makefile has a gen-deno-snapshot target that refreshes the Deno API snapshot consumed by gen-deno, which requires the deno binary.
How is workers-go tested?
With three separate targets. The test target runs the suite with GOOS=js GOARCH=wasm against a prebuilt WebAssembly executable in testdata/wasm, test-host runs the root and cmd packages on the host, and test-e2e runs a separately tagged suite with a ten minute timeout, and test-all runs all three.
What license is workers-go released under?
MIT, with a LICENSE.md file at the root of the repository. The newest listed release is v0.36.0 from 2026-09-21, and the last push was on 2026-09-23.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/syumai-workers-go)Star history chart for your README
[](https://hysenlabs.com/projects/syumai-workers-go)Was this useful?