RPC and named entrypoints
Call and host Cloudflare Workers RPC methods on named WorkerEntrypoints from Go.
Workers RPC lets one Worker call methods on another over a Service binding pointing at a named WorkerEntrypoint, or call a Durable Object’s own methods beyond fetch(). The exp/cloudflare/rpc package covers both directions: calling a remote entrypoint and hosting one implemented in Go.
TypeScript’s typed Rpc.Provider<T> stubs can’t be generated for statically-typed Go — there’s no way to derive a Go method set from an arbitrary remote class’s shape — so both sides of this package are dynamic: a caller invokes a method by name with untyped arguments, and a Go-hosted entrypoint registers its methods by name too.
Call a remote entrypoint
rpc.NewStub(bindingName) resolves a Service binding and wraps it as a *rpc.Stub. Call invokes a method and awaits its result (an RPC method’s return is always a Promise); CallJSON additionally JSON round-trips the result into a typed Go value.
stub, err := rpc.NewStub("SELF")
if err != nil {
return err
}
var sum int
if err := stub.CallJSON("add", &sum, 1, 2); err != nil {
return err
}
var greeting string
if err := stub.CallJSON("greet", &greeting, "go"); err != nil {
return err
}
Arguments are converted the way syscall/js.ValueOf converts a Go value to JS — primitives, map[string]any, []any, js.Value, and types implementing syscall/js.Wrapper are supported; anything else panics. Call returns the raw js.Value result; if it’s itself an RPC stub or function (e.g. a method returning another Rpc.Target), wrap it in another Stub via rpc.StubFromJS to keep calling into it.
rpc.StubFromJS(v) wraps an existing JS value as a *rpc.Stub directly — used for a Durable Object stub’s own RPC methods:
stub, err := ns.Get(id)
if err != nil {
return err
}
var greeting string
err = stub.RPC().CallJSON("greet", &greeting, "go")
DurableObjectStub.RPC() (cloudflare package) returns an *rpc.Stub for exactly this — see Durable Objects.
Host RPC methods in Go
A Go-hosted WorkerEntrypoint registers its methods with rpc.Register, before workers.Serve (or any other blocking call). rpc.Method takes and returns raw js.Values; rpc.MethodJSON adapts a typed function instead.
import (
"context"
"encoding/json"
"github.com/syumai/workers-go/exp/cloudflare/rpc"
)
var addMethod = rpc.MethodJSON(func(ctx context.Context, args []json.RawMessage) (int, error) {
var a, b int
json.Unmarshal(args[0], &a)
json.Unmarshal(args[1], &b)
return a + b, nil
})
var greetMethod = rpc.MethodJSON(func(ctx context.Context, args []json.RawMessage) (string, error) {
var name string
json.Unmarshal(args[0], &name)
return "hello, " + name, nil
})
func main() {
rpc.Register("MyService", map[string]rpc.Method{
"add": addMethod,
"greet": greetMethod,
})
rpc.RegisterFetch("MyService", myFetchHandler) // optional; serves MyService's own fetch()
workers.Serve(nil)
}
Since RPC arguments arrive positionally with no fixed arity known to the package, MethodJSON’s function receives each argument individually JSON-encoded as a json.RawMessage — decode as many of them, into whatever shape the method expects.
rpc.RegisterFetch(className, handler) is optional: it serves the entrypoint’s own fetch() trigger — distinct from the Worker’s regular default-export handler passed to workers.Serve — which runs when something calls .fetch() on a stub bound to this named entrypoint. A class generated without a matching RegisterFetch call rejects its fetch() calls at request time; a class with no Register-ed method for a name rejects that call.
Build and wiring
Pass -entrypoints to workers-assets-gen — semicolon-separate multiple classes, and comma-separate method names within one class (Name alone is valid too, with no RPC methods beyond the always-generated fetch()):
go run github.com/syumai/workers-go/cmd/workers-assets-gen -mode=go -entrypoints=MyService:add,greet
This appends one subclass per spec to the generated worker.mjs:
export class MyService extends GoWorkerEntrypoint {
static goClassName = "MyService";
async add(...args) { return (await this._bind()).handleRPC("add", args); }
async greet(...args) { return (await this._bind()).handleRPC("greet", args); }
async fetch(req) { return (await this._bind()).handleEntrypointFetch(req); }
}
The name passed here, the className given to rpc.Register/rpc.RegisterFetch, and (for a Service binding) wrangler.toml’s [[services]] entrypoint must all match exactly.
Point a Service binding at it in wrangler.toml — including from the same Worker, to call its own entrypoint:
compatibility_date = "2024-04-03"
[[services]]
binding = "SELF"
service = "rpc-go"
entrypoint = "MyService"
Workers RPC requires compatibility_date of 2024-04-03 or later (or the rpc compatibility flag on an earlier date).
Example project
_examples/rpc-go hosts a MyService entrypoint with add/greet RPC methods and calls it from the same Worker’s regular fetch handler through a SELF Service binding.