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

Evaluate 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) // 500

WithEnv 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


Risor © 2026 Deep Noodle