Unlocking Go's Power: How Goja Brings JavaScript to Your Go Apps
This article explores Goja, a pure‑Go JavaScript engine, detailing its features, integration with the K6 load‑testing tool, and practical code examples for embedding scripts, passing values, handling structs, invoking Go functions, error handling, and VM pooling to achieve high performance in Go applications.
Embedded script engines give developers flexibility for dynamic extensions and cross‑language integration. Goja is a lightweight, pure‑Go JavaScript engine that enables Go applications to run JavaScript code without external C/C++ dependencies.
Goja Overview
Goja is a pure Go implementation of a JavaScript runtime, compatible with ECMAScript 5.1. It runs JavaScript inside Go programs, offering natural compatibility and cross‑platform support while avoiding the overhead of native engines like V8.
Key Features
Pure Go implementation : No extra bindings or C libraries, allowing seamless embedding in any Go project.
ECMAScript 5.1 compatibility : Supports the majority of common JavaScript use cases despite lacking ES6/ES7 features.
High performance and safety : Minimal runtime overhead and reduced security surface because it avoids external native code.
Lightweight : Small footprint makes it ideal for microservices and embedded scenarios.
Typical Use Cases
Dynamic extension in web applications : Execute custom JavaScript for page rendering or complex business logic.
Automation and scripting : Write scripts to automate workflows or implement configurable rule engines.
Plugin and extension systems : Provide a scriptable plugin layer for Go applications.
Goja in K6 Load Testing
K6, an open‑source load‑testing tool, uses Goja as its JavaScript execution engine. Test scripts are written in JavaScript, parsed and run by Goja within a Go process. Because Goja implements only ES5.1, K6 adopts a synchronous execution model, wrapping I/O operations to keep scripts predictable.
Example K6 script:
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const res = http.get('https://FunTester-api.com');
check(res, { 'status was 200': (r) => r.status === 200 });
}Basic JavaScript Execution in Go
Creating a Goja runtime and running a simple script that adds two numbers:
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
script := `
function add(a, b) {
return a + b;
}
add(10, 20);
`
result, err := vm.RunString(script)
if err != nil {
panic(err)
}
fmt.Println("Result:", result) // Output: 30
}Passing Values Between Go and JavaScript
Example of passing a Go slice to JavaScript, filtering even numbers, and retrieving the result:
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
values := []int{}
for i := 1; i <= 10; i++ {
values = append(values, i)
}
script := `
values.filter((x) => {
return x % 2 === 0;
})
`
vm.Set("values", values)
result, err := vm.RunString(script)
if err != nil {
panic(err)
}
filteredValues := result.Export().([]interface{})
fmt.Println(filteredValues) // [2 4 6 8 10]
}Structs and Method Calls
Goja can expose Go structs to JavaScript, allowing field access and method invocation. The engine respects Go naming conventions and can map method names to camelCase via FieldNameMapper.
package main
import (
"fmt"
"github.com/dop251/goja"
)
type Person struct {
Name string
age int
}
func (p *Person) GetAge() int {
return p.age
}
func main() {
vm := goja.New()
person := &Person{Name: "John Doe", age: 30}
vm.Set("person", person)
script := `
const name = person.Name; // exported field
const age = person.GetAge(); // method call
name + " is " + age + " years old.";
`
result, err := vm.RunString(script)
if err != nil {
panic(err)
}
fmt.Println(result.String()) // Output: John Doe is 30 years old.
}Calling Go Functions from JavaScript
Go functions can be registered in the runtime and invoked directly from a script:
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
helloFunc := func(call goja.FunctionCall) goja.Value {
name := call.Argument(0).String()
result := fmt.Sprintf("Hello, %s!", name)
return vm.ToValue(result)
}
vm.Set("hello", helloFunc)
script := `
hello("Goja from FunTester");
`
result, err := vm.RunString(script)
if err != nil {
panic(err)
}
fmt.Println(result) // Output: Hello, Goja from FunTester!
}Error Handling
JavaScript exceptions are propagated as Go errors and can be handled using standard Go error checking:
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
script := `
function errorFunc() {
throw new Error("Something went wrong!");
}
errorFunc();
`
_, err := vm.RunString(script)
if err != nil {
fmt.Println("Caught error:", err)
}
}VM Pooling for Performance
Creating a sync.Pool of Goja runtimes reduces the cost of repeatedly initializing VMs, which is especially beneficial for high‑throughput scenarios such as K6 load testing.
package main
import (
"fmt"
"sync"
"github.com/dop251/goja"
)
var vmPool = sync.Pool{
New: func() interface{} {
vm := goja.New()
vm.Set("add", func(a, b int) int { return a + b })
return vm
},
}
func main() {
vm := vmPool.Get().(*goja.Runtime)
defer vmPool.Put(vm)
result, err := vm.RunString(`add(1, 2)`)
if err != nil {
panic(err)
}
fmt.Println(result) // Output: 3
}Overall, Goja offers a flexible, lightweight, and high‑performance JavaScript runtime for Go developers, suitable for scripting, plugin systems, and performance‑critical tools like K6.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
