# Exploring Goja: A Golang JavaScript Runtime

This post explores Goja, a JavaScript runtime library in the Golang ecosystem. Goja stands out as a powerful tool for embedding JavaScript within Go applications, offering unique advantages when manipulating data and exposing an SDK that doesn’t require a go build step.

# Background: The Need for Goja

In my project, I encountered challenges when querying and manipulating large datasets. Initially, everything was written in Go, which was efficient but became cumbersome, especially when dealing with complex JSON responses. While Go’s minimalistic approach is generally advantageous, the verbosity required for specific tasks slowed me down.

Using an embedded scripting language could simplify the process, leading me to explore various options. Lua was my first choice because of its reputation for being lightweight and embeddable. Still, I quickly found that the available Go libraries for Lua were all over the place in implementations, versions (5.1, 5.2, etc), and active support.

I then investigated other popular scripting languages in the Go ecosystem. I considered options like Expr, V8, and Starlark, but eventually, Goja emerged as the most promising candidate.

Here is the GitHub repository, where I conducted some benchmarks on these libraries, testing their performance and ease of integration with Go.

# Why Goja?

Goja won me over because of its seamless integration with Go structs. When you assign a Go struct to a value in the JavaScript runtime, Goja automatically infers the fields and methods, making them accessible in JavaScript without requiring a separate bridge layer. It leverages Go’s reflection capabilities to invoke getters and setters on these fields, offering a robust and transparent interaction between Go and JavaScript.

Let’s dive into some examples to see Goja in action. These examples highlight features I’ve found useful, but desired to have more examples in the documentation with.

# Assigning and Returning Values

To start, let’s take a simple example where we pass an array of integers from Go to the JavaScript runtime and filter out the even values.

package main

import (
	"fmt"

	"github.com/dop251/goja"
)

func main() {
	vm := goja.New()

	// Passing an array of integers from 1 to 100
	values := []int{}
	for i := 1; i <= 100; i++ {
		values = append(values, i)
	}

	// Define the JavaScript code to filter even values
	script := `
		values.filter((x) => {
			return x % 2 === 0;
		})
  `

	// Set the array in the JavaScript runtime
	vm.Set("values", values)

	// Run the script
	result, err := vm.RunString(script)
	if err != nil {
		panic(err)
	}

	// Convert the result back to a Go slice of empty interfaces
	filteredValues := result.Export().([]interface{})

	fmt.Println(filteredValues)
	// Outputs: [2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100]

	first := filteredValues[0].(int64)
	fmt.Println(first)
}

In this example, you can see that iterating through an array in Goja doesn’t require explicit type annotations. Goja is able to infer the type of the array based on its content, thanks to Go’s reflection mechanism. When filtering the values and returning the result, Goja converts the result back to an array of empty interfaces ([]interface{}). This is because Goja needs to handle JavaScript’s dynamic typing within Go’s static type system.

If you need to work with the resulting values in Go, you’ll have to perform type assertions to extract the integers. Internally, Goja represents all integers as int64.

# Structs and Method Calls

Next, let’s explore how Goja handles Go structs, particularly focusing on methods and exported fields.

package main

import (
    "fmt"
    "github.com/dop251/goja"
)

type Person struct {
    Name string
    age  int
}

// Method to get the age (unexported)
func (p *Person) GetAge() int {
    return p.age
}

func main() {
    vm := goja.New()

    // Create a new Person instance
    person := &Person{
        Name: "John Doe",
        age:  30,
    }

    // Set the Person struct in the JavaScript runtime
    vm.Set("person", person)

    // JavaScript code to access the struct's fields and methods
    script := `
        const name = person.Name;    // Access exported field
        const age = person.GetAge(); // Access unexported field via getter
        name + " is " + age + " years old.";
    `

    result, err := vm.RunString(script)
    if err != nil {
        panic(err)
    }

    fmt.Println(result.String()) // Outputs: John Doe is 30 years old.
}

In this example, I’ve defined a Person struct with an exported Name field and an unexported age field. The GetName method is exported. When accessing these fields and methods from JavaScript, Goja adheres to the naming conventions on the struct. The method GetAge is accessed as GetName.

There is a pattern for making the Javascript naming convention of camel case translate to Golang naming convention via FieldNameMapper. This allows for the Go method GetAge to be called as getAge in the javascript invocation.

# Exception Handling

When an exception occurs in JavaScript, Goja uses standard Go error handling to manage it. Let’s explore an example of a runtime exception—division by zero.

package main

import (
	"errors"
	"fmt"

	"github.com/dop251/goja"
)

// JavaScript code that triggers a division by zero error
const script = `
	// Using BigInt notation in JavaScript
	const a = 1n / 0n;
`

func main() {
	vm := goja.New()

	// Execute the JavaScript code
	_, err := vm.RunString(script)

	// Handle any errors that occur
	var exception *goja.Exception
	if errors.As(err, &exception) {
		fmt.Printf("JavaScript error: %s\n", exception.Error())
		// Output: JavaScript error: RangeError: Division by zero at <eval>:1:1(3)
	} else if err != nil {
		// Handle other types of errors (if any)
		fmt.Printf("Error: %s\n", err.Error())
	}
}

The error value returned is of type *goja.Exception, which provides information about the JavaScript exception that was raised and where it failed. While I haven’t found a strong need to inspect these errors beyond logging them to services like New Relic or DataDog, Goja does offer the tools to do so if necessary.

Additionally, Goja can raise other types of exceptions, such as *goja.StackOverflowError, *goja.InterruptedError, and *goja.CompilerSyntaxError, which correspond to specific issues related to the interpreter. These exceptions can be useful to handle and report, especially when dealing with clients that execute JavaScript code.

# Sandbox User Code with a Pool of VMs

While developing my application, I noticed that initializing the VM took a significant amount of time. Each VM required global modules that needed to be available to the user at runtime. Go provides sync.Pool to help reuse objects, making it a perfect fit for my use case to avoid the overhead of heavy initialization.

Here’s an example of a pool of Goja VMs:

package main

import (
	"fmt"
	"sync"

	"github.com/dop251/goja"
)

var vmPool = sync.Pool{
	New: func() interface{} {
		vm := goja.New()

		// Define a global function available in every VM
		vm.Set("add", func(a, b int) int {
			return a + b
		})

		// ... other global values set ...

		return vm
	},
}

func main() {
	vm := vmPool.Get().(*goja.Runtime)
	// Put the VM back into the pool for reuse
	defer vmPool.Put(vm)

	script := `
		const result = add(5, 10);
		result;
	`

	value, err := vm.RunString(script)
	if err != nil {
		panic(err)
	}

	fmt.Println("Result:", value.Export())
	// Result: 15
}

Since sync.Pool is well-documented, let’s focus on the JavaScript runtime. In this example, the user declares a variable result, and its value is returned. However, we encounter a limitation: the VM cannot be reused as is.

The global namespace has been polluted with the variable result. If I rerun the same code with the same pool, I receive the following error: SyntaxError: Identifier 'result' has already been declared at <eval>:1:1(0). There is a GitHub issue that recommends clearing the value of result each time. However, I found this pattern impractical due to the added complexity when dealing with user-provided code.

Until now, the examples I’ve given have been demonstrations of predefined code. My application, however, allows users to provide their own code to run within the Goja runtime. This required some experimentation, exploration, and the adoption of patterns to avoid the “already declared” error.

value, err := vm.RunString("(function() {" + userCode + "})()")
if err != nil {
	panic(err)
}

The final solution for sandboxing user code involves executing the userCode within an anonymous function in its own scope. Since the function isn’t named, it isn’t assigned globally, and therefore doesn’t require cleanup. After some benchmark testing, I confirmed that garbage collection effectively cleans it up as well.

# Conclusion

I’ve unlocked a flexible and efficient way to handle complex scripting tasks without sacrificing performance. This approach significantly reduces the time spent on cumbersome tasks, giving you more time to focus on other important aspects, and enhances the overall user experience by providing a seamless and responsive scripting environment.

My experience with the nuances of Goja helps you get started quickly!