探索 Goja:Golang 中的 JavaScript 运行时
这篇文章探讨了 Goja,这是 Golang 生态系统中的一个 JavaScript 运行时库。Goja 作为一个在 Go 应用程序中嵌入 JavaScript 的强大工具,提供了独特的优势,尤其是在操作数据和提供不需要 go build 过程中的 SDK。
背景:Goja 的需求
在我的项目中,我在查询和操作大型数据集时遇到了挑战。最初,一切都是用 Go 编写的,这在效率上是有利的,但在处理复杂的 JSON 响应时变得笨拙。虽然 Go 的极简主义方法通常是一个优势,但特定任务所需的冗长性减慢了我的速度。
使用嵌入式脚本语言可以简化这个过程,这让我探索了各种选项。Lua 是我的首选,因为它以轻量级和可嵌入而闻名。但我很快发现,Go 中的 Lua 库在实现、版本(5.1、5.2 等)和活跃支持方面参差不齐。
然后我调查了 Go 生态系统中其他流行的脚本语言。我考虑了 Expr、V8 和 Starlark 等选项,但最终 Goja 成为了最有希望的候选者。
这里是我在这些库上进行基准测试的 GitHub 仓库,测试了它们的性能和与 Go 的集成便利性。
为什么选择 Goja?
Goja 因其与 Go 结构体的无缝集成而赢得了我的青睐。当你将一个 Go 结构体分配给 JavaScript 运行时中的一个值时,Goja 会自动推断字段和方法,使它们在 JavaScript 中可访问,而不需要单独的桥接层。它利用 Go 的反射能力来调用这些字段上的 getter 和 setter,提供了 Go 和 JavaScript 之间强大而透明的交互。
让我们通过一些示例来看看 Goja 的实际应用。这些示例突出了我发现有用的特性,但我希望在文档中有更多示例。
赋值和返回值
首先,让我们来看一个简单的例子,我们将一个从 1 到 100 的整数数组从 Go 传递到 JavaScript 运行时,并过滤出偶数值。
1 | package main |
在这个例子中,你可以看到在 Goja 中遍历数组不需要显式类型注释。Goja 能够根据内容推断数组的类型,这得益于 Go 的反射机制。在过滤值并返回结果时,Goja 将结果转换回空接口切片([]interface{})。这是因为 Goja 需要在 Go 的静态类型系统中处理 JavaScript 的动态类型。
如果你需要在 Go 中处理结果值,你将不得不执行类型断言以提取整数。在内部,Goja 将所有整数表示为 int64。
结构体和方法调用
接下来,让我们探索 Goja 如何处理 Go 结构体,特别关注方法和导出字段。
1 | package main |
在这个例子中,我定义了一个 Person 结构体,它有一个导出的 Name 字段和一个未导出的 age 字段。GetName 方法是导出的。当从 JavaScript 访问这些字段和方法时,Goja 遵循结构体上的命名约定。方法 GetAge 被访问为 GetName。
有一个模式是通过 FieldNameMapper 将 JavaScript 命名约定的小驼峰式转换为 Golang 命名约定。这允许 Go 方法 GetAge 在 JavaScript 调用中被调用为 getAge。
异常处理
当 JavaScript 中发生异常时,Goja 使用标准的 Go 错误处理来管理它。让我们探索一个运行时异常的例子——除以零。
1 | package main |
返回的错误值是 *goja.Exception 类型,它提供了有关引发 JavaScript 异常的信息以及失败的位置。虽然我没有强烈的需求去检查这些错误之外的记录它们到像 New Relic 或 DataDog 这样的服务,但 Goja 确实提供了这样做的工具,如果需要的话。
此外,Goja 可以引发其他类型的异常,如 goja.StackOverflowError、goja.InterruptedError 和 *goja.CompilerSyntaxError,这些异常对应于解释器的特定问题。这些异常在处理执行 JavaScript 代码的客户端时,有助于处理和报告,特别是。
使用 VM 池沙箱用户代码
在开发我的应用程序时,我注意到初始化 VM 需要相当长的时间。每个 VM 都需要在运行时对用户可用的全局模块。Go 提供了 sync.Pool 来帮助重用对象,这非常适合我的情况,避免了沉重的初始化开销。
下面是一个 Goja VM 池的例子:
1 | package main |
由于 sync.Pool 有详细的文档,让我们专注于 JavaScript 运行时。在这个例子中,用户声明了一个变量 result,它的值被返回。然而,我们遇到了一个限制:VM 不能像现在这样重用。
全局命名空间已经被变量 result 污染了。如果我用同一个池重新运行相同的代码,我会收到以下错误:SyntaxError: Identifier ‘result’ has already been declared at
到目前为止,我给出的例子都是预定义代码的演示。然而,我的应用程序允许用户在 Goja 运行时中提供自己的代码。这需要一些实验、探索和采用模式来避免“已经声明”的错误。
1 | value, err := vm.RunString("(function() {" + userCode + "})()") |
沙箱用户代码的最终解决方案涉及在它自己的范围内执行 userCode 在一个匿名函数中。由于函数没有命名,它没有被全局分配,因此不需要清理。经过一些基准测试后,我确认垃圾收集有效地清理了它。
结论
我们已经解锁了一种灵活高效的方式来处理复杂的脚本任务,而不会牺牲性能。这种方法大大减少了在繁琐任务上花费的时间,让你有更多的时间专注于其他重要的方面,并通过提供无缝和响应迅速的脚本环境来增强整体用户体验。