Go bindings for Ray Core.
- Pure Go — Build Ray applications in Go without touching Python
- Seamless Polyglot — Hybrid Python–Go development with cross-language Task/Actor invocation
- Strong Compatibility — Clean implementation with zero hacks
- Type-safe Remote Calls — Compile-time safety and full IDE support via code generation
Init a go project:
mkdir goray-app && cd goray-app
go mod init rayappAdd a main.go file:
package main
import (
"fmt"
"github.com/ray4go/go-ray/ray"
)
type Tasks struct{}
func (Tasks) Square(x float64) float64 {
return x * x
}
func driver() int {
obj := ray.RemoteCall("Square", 2.0)
res, err := ray.Get1[float64](obj)
fmt.Printf("Square(2.0) = %f, err: %v\n", res, err)
return 0
}
func init() {
ray.Init(Tasks{}, nil, driver)
}
func main() {}Build and run the application:
go mod tidy
go build -buildmode=c-shared -o rayapp .
pip install "goray[ray]"
goray rayappgo get github.com/ray4go/go-ray/ray
pip install "goray[ray]"Requires Go 1.21+ and Python 3.10+;
package main
import (
"fmt"
"log"
"time"
"github.com/ray4go/go-ray/ray"
)
// ray tasks register
type Tasks struct{}
func (_ Tasks) TheAnswerOfWorld() int64 {
time.Sleep(1 * time.Second)
return 42
}
func (_ Tasks) Divide(a, b int64) (int64, int64) {
return a / b, a % b
}
// ray actors register
type Actors struct{}
func (_ Actors) Counter(n int) *Counter {
return &Counter{num: n}
}
// ray actor
type Counter struct {
num int
}
func (c *Counter) Incr(n int) int {
fmt.Printf("Incr %d -> %d\n", c.num, c.num+n)
c.num += n
return c.num
}
func (c *Counter) Decr(n int) int {
fmt.Printf("Decr %d -> %d\n", c.num, c.num-n)
c.num -= n
return c.num
}
func init() {
// Initialize and register Ray Tasks, Actors, and the driver
ray.Init(Tasks{}, Actors{}, driver)
}
func driver() int {
// ray task remote call
answerObjRef := ray.RemoteCall("TheAnswerOfWorld")
objRef := ray.RemoteCall("Divide", answerObjRef, 5, ray.Option("num_cpus", 2), ray.Option("memory", 100*1024*1024))
res, remainder, err := ray.Get2[int64, int64](objRef)
if err != nil {
log.Panicf("remote task error: %v", err)
}
fmt.Printf("call Divide -> %#v, %#v \n", res, remainder)
// ray actor remote call
cnt := ray.NewActor("Counter", 1)
obj := cnt.RemoteCall("Incr", 1)
var res2 int
err2 := obj.GetInto(&res2)
fmt.Println("Incr ", res2, err2)
obj2 := cnt.RemoteCall("Incr", 1)
obj3 := cnt.RemoteCall("Incr", obj2)
fmt.Println(obj3.GetAll())
return 0
}
// main function won't be called but cannot be omitted (it's required only for compilation)
func main() {}Use ray.Init(taskRegister, actorRegister, driver, ...rayOptions) to register Ray tasks, actors, and the driver and init the Ray environment:
- All exported methods on the
taskRegisterare registered as Ray tasks. - Exported methods on the
actorRegisterare used to create actors; each method serves as the actor’s constructor. - The driver function must have signature
func() int. It is called when the ray application starts and should return an integer as exit code. rayOptionsare used to provide theray.init()options. In most cases, it's enough to pass no options.
ray.Init() should be called in the main package's init() function. All other GoRay APIs must be called within the
driver function and its spawned remote actors/tasks.
The main function of the main package won't be called but cannot be omitted.
Tasks:
- Use
ray.RemoteCall(taskName, args...) -> ObjectRefto invoke a Ray task asynchronously. - Configure tasks by appending zero or more
ray.Option(key, value)arguments (equivalent to Ray’s.options(...)). - Parameters and return values may be composite types or structs (struct fields must be exported). Variadic arguments and multiple return values are supported.
Actors:
- Use
ray.NewActor(actorTypeName, args...) -> ActorHandleto create a remote actor. Ray options are also supported viaray.Option. actorTypeNamematches the constructor method name ofactorsregistered viaray.Init(tasks, actors, driver). Constructor arguments (excluding options) are passed to that method.- Use
actorHandle.RemoteCall(methodName, args...) -> ObjectRefto invoke actor methods. Semantics matchray.RemoteCall().
Remote calls return an ObjectRef, which represents a future. Because Go supports multiple return values, ObjectRef
does too. Retrieve results in any of these ways:
-
Pass pointers with
ObjectRef.GetInto(ptrs...) -> error. -
Use typed helpers:
- For no returns remote call:
ObjectRef.Get0() -> error - One return:
ray.Get1[T]() -> (T, error) - Two returns:
ray.Get2[T1, T2]() -> (T1, T2, error) - And so on.
- For no returns remote call:
-
Use
ObjectRef.GetAll() -> []anyand assert to concrete types.
You may pass ObjectRef as arguments to RemoteCall(), but only when it has a single return value and the result type
is compatible with the parameter type.
Passing an ActorHandle as a parameter to RemoteCall() is not supported yet. A workaround is to create a named actor
(via NewActor(typeName, ray.Option("name", actorName))) and pass the actor's name to RemoteCall().
In the remote task/actor, use ray.GetActor(actorName) to get the handle.
Arguments and return values are serialized via msgpack:
- Supported: integers, floats, booleans, strings, binary data, slices, maps, nil, structs, and their pointer types
- Map keys must be strings or integers
Put(data any) -> (SharedObject, error)— Store an object in Ray’s object store. The returned SharedObject can be passed to other tasks/actors (just likeObjectRef).Wait(objRefs []ObjectRef, requestNum int, opts ...*option) -> (ready []ObjectRef, pending []ObjectRef, error)— Wait until the specified number ofObjectRefs complete.ObjectRef.Cancel(opts ...*option) -> error— Cancel a remote task (task or actor method).GetActor(name string, opts ...*option) -> (*ActorHandle, error)— Get a handle to a named actor instance.actorHandle.Kill(opts ...*option)— Terminate an actor instance.
Where *option represents Ray API parameters, created with ray.Option(name string, val any). For supported
parameters, see the Ray official API docs.
go build -buildmode=c-shared -o ./build/rayapp .You must compile with -buildmode=c-shared to produce a shared library.
- Install the official Ray Python SDK:
pip install "ray[default]"- Install the go-ray Python driver:
pip install goray- Run the Ray job:
goray ./build/rayappPrerequisite: Install the go-ray Python driver on the cluster.
Submit a job:
export RAY_ADDRESS="http://RAY_CLUSTER_ADDRESS" # Replace with your cluster address
ray job submit --working-dir=./ -- goray --cluster ./build/rayappNotes:
- The compiled binary must be inside the working directory (or a subdirectory) and not excluded by .gitignore.
- The binary path passed to
goraymust be relative to--working-dir.
Command details
To run Go in Ray, the compiled binary must be distributed to all nodes. --working-dir=./ packages and ships files from
the working directory to each node.
goray is a Python entry point that bridges Python and Go. The binary path passed to goray is resolved on the
cluster. Because the job’s working directory is not known in advance, a relative path is required. If you distribute the
binary by other means, you may use an absolute path.
Cross-language features:
- Cross-language remote call of Ray tasks and actors
- Call Python Ray tasks from Go; call Go Ray tasks from Python
- Create and call Python actors from Go; create and call Go actors from Python
- Pass Go
ObjectRefs to Python, and vice versa - Create a named actor in Go, and get its handle in Python, and vice versa
- Local cross-language calls (in-process)
- Go calls Python functions in-process
- Python calls Go functions in-process
- Python instantiates Go classes and calls their methods in-process
More details and examples:
APIs such as ray.RemoteCall(), ray.NewActor(), and actorHandle.RemoteCall() use string for names and any for
arguments, and require manual casting of results. This results in poor IDE support and a suboptimal developer
experience.
GoRay provides a goraygen CLI to generate type-safe wrappers for tasks and actors.
Workflow:
- Annotate the struct with
// raytasksand the actor register struct with// rayactors. - Generate wrappers:
go install github.com/ray4go/goraygen
goraygen /path/to/your/package/goraygen generates a ray_workloads_wrapper.go file in the package directory, containing wrappers for all tasks and
actors.
The Ray Core uses distributed reference counting to manage object lifetimes; once an object has no remaining references, it is garbage collected from the Ray object store.
However, in golang, we can't track the lifetime of objects. So we use a simple and different approach to manage object references.
In GoRay, by default, an object reference will be automatically released once it has been retrieved or passed to another task.
Subsequent access to a released ObjectRef will cause ErrObjectRefNotFound.
You can use ObjectRef.DisableAutoRelease() to turn off this behavior, the object reference will persist until task ends,
or you can call ObjectRef.Release() manually when you no longer need the reference in the current task.
Parameter and return types
- Prefer concrete types (primitive types, composites, structs, and their pointers) over interface types for parameters
- Do not use interface types as return values except for
anytype.
Error handling
Do not return error (as it is an interface) directly from Ray Task functon / Actor methods. Instead, return concrete
types to indicate errors (e.g., return int for error code), or just panic in your code.
When ObjectRef.GetAll() / ObjectRef.Getinto() / ray.GetN(obj) is called on a panic-ed task/actor, it will return an error.
You can use fmt.Printf("%+v", err) to print the error stack trace.
Go can compile to shared libraries and expose C APIs via cgo. Python can call C libraries via ctypes. This enables Python to call into Go. With Go reflection, Python can dynamically invoke Go functions/methods.
By passing ctypes-based C callback functions from Python into Go, Go can call back into Python.
msgpack is used to serialize arguments and return values, enabling cross-language data exchange between Python and Go.