Goja,一个在Golang中嵌入JavaScript的运行时库
本文探讨了 Golang 生态系统中的 JavaScript 运行时库 Goja[^1]。Goja 作为一个在 Go 应用程序中嵌入 JavaScript 的强大工具脱颖而出,在操作数据和提供无需 go build
步骤的 SDK 方面具有独特优势。
背景:为什么需要 Goja
在我的项目中,在查询和操作大型数据集时遇到了挑战。最初,所有内容都是用 Go 编写的,这很高效,但在处理复杂的 JSON 响应时变得麻烦。虽然 Go 的极简主义方法通常是有利的,但特定任务所需的冗长性降低了我的速度。
使用嵌入式脚本语言可以简化这个过程,这促使我探索各种选择。Lua 是我的首选,因为它以轻量级和可嵌入而闻名。但我很快发现,Go 中可用的 Lua 库在实现、版本(5.1、5.2 等)和活跃支持方面都各不相同。
随后我调查了 Go 生态系统中其他流行的脚本语言,如 Expr[^2]、V8[^3] 和 Starlark[^4],最终 Goja 成为了最有前途的候选者。
这里是 GitHub 仓库[^5],我在其中对这些库进行了基准测试,测试它们的性能和与 Go 的集成便利性。
为什么选择 Goja?
Goja 之所以赢得我的青睐,是因为它与 Go 结构体的无缝集成。当你将 Go 结构体分配给 JavaScript 运行时中的值时,Goja 会自动推断字段和方法,使它们在 JavaScript 中可访问,而无需单独的桥接层。它利用 Go 的反射能力来调用这些字段的 getter 和 setter,提供了 Go 和 JavaScript 之间强大而透明的交互。
示例 1: 分配和返回值
让我们看一个简单的例子,将一个整数数组从 Go 传递到 JavaScript 运行时,并过滤出偶数值。
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
// 将 Go 切片分配给 JavaScript 变量
err := vm.Set("numbers", []int{1, 2, 3, 4, 5})
if err != nil {
panic(err)
}
// 在 JavaScript 中执行过滤操作
v, err := vm.RunString(`
numbers.filter(n => n % 2 === 0)
`)
if err != nil {
panic(err)
}
// 将结果转换回 Go 切片
result := v.Export().([]interface{})
fmt.Println(result) // 输出: [2 4]
}
在这个例子中,Goja 能够根据其内容推断数组的类型,得益于 Go 的反射机制。过滤值并返回结果时,Goja 将结果转换回空接口数组 ([]interface{}
),这为 Go 中处理 JavaScript 的动态类型提供了便利。
示例 2: 结构体和方法调用
接下来,让我们探讨 Goja 如何处理 Go 结构体,特别是方法和导出字段。
package main
import (
"fmt"
"github.com/dop251/goja"
)
type Person struct {
Name string
age int
}
func (p *Person) GetName() string {
return p.Name
}
func main() {
vm := goja.New()
person := &Person{Name: "Alice", age: 30}
err := vm.Set("person", person)
if err != nil {
panic(err)
}
v, err := vm.RunString(`
person.Name + " is " + person.GetName()
`)
if err != nil {
panic(err)
}
fmt.Println(v.Export()) // 输出: Alice is Alice
}
在这个例子中,Goja 可以透明地访问 Go 结构体的导出字段和方法。未导出的字段 age
无法从 JavaScript 中访问,但导出的 Name
字段和 GetName
方法是可以使用的。
异常处理
当 JavaScript 中发生异常时,Goja 使用标准的 Go 错误处理机制。以下示例展示了如何捕获 JavaScript 中的运行时异常——例如除以零。
package main
import (
"fmt"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
_, err := vm.RunString(`
1 / 0
`)
if err != nil {
fmt.Printf("Error: %v\n", err)
// 输出: Error: RangeError: Division by zero at <eval>:2:9(1)
}
}
Goja 的错误值类型为 *goja.Exception
,提供了有关 JavaScript 异常及其发生位置的信息。对于特定情况,Goja 还可以引发其他类型的异常,如 *goja.StackOverflowError
和 *goja.CompilerSyntaxError
,它们有助于处理执行 JavaScript 代码时的特殊问题。
使用 VM 池沙箱化用户代码
初始化 Goja 虚拟机 (VM) 的开销较大。为此,我们可以使用 Go 的 sync.Pool
来优化性能。以下是使用 sync.Pool
的一个简单示例:
package main
import (
"fmt"
"github.com/dop251/goja"
"sync"
)
var vmPool = sync.Pool{
New: func() interface{} {
return goja.New()
},
}
func main() {
vm := vmPool.Get().(*goja.Runtime)
defer vmPool.Put(vm)
v, err := vm.RunString(`
var result = 42;
result;
`)
if err != nil {
panic(err)
}
fmt.Println(v.Export()) // 输出: 42
}
使用 sync.Pool
时,需注意 VM 的全局命名空间问题。为解决这个问题,我们可以将用户代码封装在匿名函数中运行,避免命名冲突。
package main
import (
"fmt"
"github.com/dop251/goja"
"sync"
)
var vmPool = sync.Pool{
New: func() interface{} {
return goja.New()
},
}
func runUserCode(userCode string) (interface{}, error) {
vm := vmPool.Get().(*goja.Runtime)
defer vmPool.Put(vm)
v, err := vm.RunString(fmt.Sprintf(`
(function() {
%s
})();
`, userCode))
if err != nil {
return nil, err
}
return v.Export(), nil
}
func main() {
userCode := `
var x = 10;
var y = 20;
return x + y;
`
result, err := runUserCode(userCode)
if err != nil {
panic(err)
}
fmt.Println(result) // 输出: 30
}
结论
Goja 提供了一种灵活且高效的方式来在 Go 程序中处理 JavaScript 脚本任务。它无缝的 Go 集成和动态特性极大地提升了处理复杂任务的效率。通过使用 Goja,你可以在不牺牲性能的情况下优化代码执行和用户体验。