# 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!