Embedding Risor in Go
Adding Risor to a Go application takes minutes. This guide walks you from a one-line eval to compiling scripts for high-concurrency reuse.
Install the Module
go get github.com/deepnoodle-ai/risor/v2Evaluate a Script
risor.Eval is the simplest entry point. It compiles and runs a script in
a single call.
package main
import (
"context"
"fmt"
"github.com/deepnoodle-ai/risor/v2"
)
func main() {
result, err := risor.Eval(context.Background(), `[1, 2, 3].map(x => x * 2)`)
if err != nil {
panic(err)
}
fmt.Println(result) // [2, 4, 6]
}Notice there's no environment setup here. By default, scripts run in an empty sandbox: no built-in functions, no modules, no external state.
Add the Standard Library
Pass risor.Builtins() to add the standard library (len, keys,
sorted, type, and the rest).
source := `
let items = ["cherry", "apple", "banana"]
sorted(items)
`
result, err := risor.Eval(ctx, source, risor.WithEnv(risor.Builtins()))
fmt.Println(result) // [apple, banana, cherry]risor.Builtins() returns a map[string]any containing all the standard
built-in functions. You can inspect or modify this map before passing it in.
Provide Custom Variables
Inject your own variables and functions by passing a custom map with
risor.WithEnv.
env := map[string]any{
"price": 100.0,
"quantity": 5,
}
result, err := risor.Eval(ctx, "price * quantity", risor.WithEnv(env))
fmt.Println(result) // 500WithEnv options are additive. If the same key appears in multiple maps,
the last value wins. Layer the standard library with your own additions:
result, err := risor.Eval(ctx, source,
risor.WithEnv(risor.Builtins()),
risor.WithEnv(map[string]any{"user": currentUser}),
)Compile Once, Run Many Times
For hot paths, separate compilation from execution. risor.Compile produces a
*vm.Code value that is immutable and safe for concurrent use.
// Compile the script once
code, err := risor.Compile(ctx, script, risor.WithEnv(risor.Builtins()))
if err != nil {
return err
}
// Run it with different inputs
for _, input := range inputs {
env := risor.Builtins()
env["input"] = input
result, err := risor.Run(ctx, code, risor.WithEnv(env))
if err != nil {
return err
}
fmt.Println(result)
}Concurrent Execution
Compiled bytecode is safe to share across goroutines with no synchronization.
Each risor.Run call spins up its own VM instance.
code, err := risor.Compile(ctx, script, risor.WithEnv(templateEnv))
if err != nil {
return err
}
var wg sync.WaitGroup
for i, input := range inputs {
wg.Add(1)
go func(idx int, val int) {
defer wg.Done()
env := risor.Builtins()
env["input"] = val
result, err := risor.Run(ctx, code, risor.WithEnv(env))
if err != nil {
log.Printf("goroutine %d error: %v", idx, err)
return
}
log.Printf("goroutine %d result: %v", idx, result)
}(i, input)
}
wg.Wait()Wrapping Go Structs
Pass Go struct pointers directly into the Risor environment. Risor wraps them
as GoStruct objects, automatically exposing exported fields and methods to
scripts.
type Service struct {
Name string
}
func (s *Service) Greet(name string) string {
return fmt.Sprintf("Hello from %s, %s!", s.Name, name)
}
svc := &Service{Name: "MyApp"}
result, err := risor.Eval(ctx, `svc.Greet("world")`,
risor.WithEnv(map[string]any{"svc": svc}),
)
fmt.Println(result) // Hello from MyApp, world!Next Steps
- API Reference: all public functions and options
- Type Registry: custom Go/Risor type conversions
- Resource Limits: step limits, stack depth, and timeouts
- Syntax Restriction: restrict which language constructs scripts can use
- Observer: execution tracking for profiling and debugging