在现代软件开发中,嵌入式脚本引擎为开发者提供了极大的灵活性。无论是需要动态扩展的应用程序,还是需要跨语言集成的项目,脚本引擎的引入让复杂系统得以更加灵活地运作。在 Golang 生态系统中,Goja 作为一个高效且轻量的 JavaScript 引擎,恰到好处地为 Go 开发者提供了运行 JavaScript 代码的能力。

这篇文章我将深入探讨 Goja,它是 Golang 生态系统中的一个强大的 JavaScript 运行时库。Goja 作为一种在 Go 应用中嵌入 JavaScript 的工具,凭借其独特的优势脱颖而出,尤其在数据处理方面表现出色,并且它提供了一个无需进行 Go 构建步骤的 SDK。

Goja 介绍

Goja 是由 Golang 实现的纯 JavaScript 引擎,能够在 Go 应用中嵌入并运行 JavaScript 代码。与传统的 V8 或 SpiderMonkey 不同,Goja 并不依赖外部的 C/C++ 库,而是完全用 Go 编写,这使得它在 Golang 环境下具有天然的兼容性和跨平台优势。

下面是 Goja 的特点:

  1. 纯 Go 实现:Goja 的最大亮点就是完全用 Go 语言编写,不需要额外的依赖或绑定。这意味着它能够无缝嵌入到任何 Go 应用中,不用担心复杂的跨语言调用或兼容性问题。
  2. ECMAScript 5.1 兼容:虽然 Goja 实现的是 ECMAScript 5.1 标准,并不支持最新的 ES6 或 ES7 特性,但其提供的 API 和功能已经足够应对大多数常见的 JavaScript 使用场景。
  3. 高性能与安全性:Goja 设计简洁,运行时性能表现相当出色,尤其在嵌入式场景中表现更佳。此外,由于它是纯 Go 实现的,不涉及复杂的外部库调用,减少了潜在的安全漏洞。
  4. 轻量与高效:相比于其他重量级的 JavaScript 引擎,Goja 的体量较小,资源占用低,非常适合嵌入式和微服务架构的应用场景。

Goja 的应用场景

  1. Web 应用中的动态扩展:Goja 可以在 Web 服务器中执行 JavaScript 代码,从而支持动态页面渲染、处理复杂的业务逻辑,甚至允许用户上传并执行自定义脚本。
  2. 自动化与脚本化:通过嵌入 Goja,开发者可以编写自动化任务脚本,处理复杂的业务流程,或者为系统添加可配置的规则引擎。
  3. 插件与扩展系统:Goja 能够作为一个轻量的脚本引擎,为 Go 应用添加脚本化的扩展功能。开发者可以使用 JavaScript 为核心应用编写插件,实现灵活的功能扩展。

Goja 与 K6

可能大家对 Goja 比较陌生,但是说起性能测试工具 K6 应该不会陌生。Goja 在 K6 中扮演了核心角色,因为 K6 的 JavaScript 执行引擎正是基于 Goja 构建的。K6 是一个开源的负载测试工具,允许用户使用 JavaScript 编写测试脚本,用来模拟虚拟用户对应用进行压力测试和性能分析。而 Goja 提供了 K6 执行 JavaScript 代码的能力,使其能够灵活地处理各种测试任务。

K6 的测试脚本是用 JavaScript 编写的,Goja 作为其运行时引擎,可以解析并执行这些 JavaScript 代码。通过 Goja,K6 能够在 Golang 环境下流畅运行 JavaScript,从而让用户使用熟悉的 JavaScript 来定义复杂的测试逻辑。

由于 Goja 基于 ECMAScript 5.1,不支持原生的异步操作(如 Promise 和 async/await),因此 K6 设计了自己的同步执行模型。K6 会将所有 I/O 操作(例如 HTTP 请求、文件读取)进行封装,以同步的方式运行,保证脚本的简单性和可预测性。

K6 使用 Goja 提供了许多内置的模块,用户可以在脚本中调用这些模块执行 HTTP 请求、处理数据、生成负载等操作。Goja 允许 K6 将自定义的 Go 函数暴露给 JavaScript 环境,使用户能够在测试脚本中调用这些函数。

Goja 的轻量特性非常适合 K6 这样的工具,它不需要引入像 V8 这样复杂且资源消耗大的 JavaScript 引擎,避免了内存占用过高的问题。对于大规模并发测试场景,这种轻量化的设计尤其重要。

下面是一个简单的 K6 压测脚本:

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 });
}

Goja 在 K6 中的应用是一个极好的实践案例,展示了如何在高并发、负载测试场景中使用轻量的 JavaScript 引擎来提供高效的脚本执行能力。它为 K6 提供了灵活的脚本化测试支持,使开发者能够使用熟悉的 JavaScript 语法编写复杂的负载测试脚本,同时保持了工具的高效性和轻量性。

Goja 本 Go

当你在 JavaScript 运行时中将一个 Go 结构体赋值给某个变量时,Goja 会自动推断该结构体的字段和方法,使它们无需额外的桥接层就能在 JavaScript 中访问。它利用了 Go 的反射机制,能够在这些字段上调用 getter 和 setter,从而在 Go 和 JavaScript 之间实现强大且透明的交互。

接下来我们通过一些示例来看看 Goja 的实际应用。

基本 JavaScript 代码执行

下面我们来试试如何在 Go 语言中使用 JavaScript 计算两个数之和。

package main

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

func main() {
    vm := goja.New() // 创建 Goja 运行时

    // 定义一个简单的 JavaScript 脚本
    script := `
        function add(a, b) {
            return a + b;
        }
        add(10, 20);
    `

    // 执行 JavaScript 代码
    result, err := vm.RunString(script)
    if err != nil {
        panic(err)
    }

    fmt.Println("Result:", result) // 输出: 30
}

控制台打印信息:

Result: 30

值的传递

下面我们来看在使用 Goja 过程中使用值传递

package main  

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

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

    // 传递从 1 到 10 的整数数组  
    values := []int{}  
    for i := 1; i <= 10; i++ {  
       values = append(values, i)  
    }  

    // 定义筛选偶数值的 JavaScript 代码  
    script := `  
       values.filter((x) => {          return x % 2 === 0;       })  `    // 将数组设置到 JavaScript 运行时中  
    vm.Set("values", values)  

    // 运行脚本  
    result, err := vm.RunString(script)  
    if err != nil {  
       panic(err)  
    }  

    // 将结果转换回 Go 的空接口切片  
    filteredValues := result.Export().([]interface{})  

    fmt.Println(filteredValues)  
}

在这个例子中,可以看到在 Goja 中迭代数组时并不需要显式的类型注释。Goja 能够基于数组的内容推断出类型,这得益于 Go 的反射机制。当筛选出偶数并返回结果时,Goja 会将结果转换为一个空接口的切片([]interface{})。这是因为 Goja 需要在 Go 的静态类型系统中处理 JavaScript 的动态类型。

结构体和方法调用

接下来,我们来研究 Goja 如何处理 Go 的结构体,尤其是方法和导出的字段。

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 := &Person{
        Name: "John Doe",
        age:  30,
    }

    // 将 Person 结构体设置到 JavaScript 运行时中
    vm.Set("person", person)

    // 访问结构体字段和方法的 JavaScript 代码
    script := `
        const name = person.Name;    // 访问导出的字段
        const age = person.GetAge(); // 通过 getter 访问未导出的字段
        name + " is " + age + " years old.";
    `

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

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

在这个例子中,我定义了一个包含导出字段 Name 和未导出字段 agePerson 结构体。 GetAge 方法则是导出的。在 JavaScript 中访问这些字段和方法时,Goja 遵循结构体的命名约定,方法 GetAge 在 JavaScript 中仍然保持为 GetAge

值得一提的是,Goja 提供了一种模式,允许通过 FieldNameMapper 将 Go 的方法名与 JavaScript 中的驼峰命名法进行转换。例如,Go 中的 GetAge 方法在 JavaScript 调用时可以作为 getAge

调用 Go 方法

下面这个例子,我们来探索一下,如何在 JavaScript 调用 Go 语言的方法。

package main  

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

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

    // 定义一个 Go 函数  
    helloFunc := func(call goja.FunctionCall) goja.Value {  
       name := call.Argument(0).String()  
       result := fmt.Sprintf("Hello, %s!", name)  
       return vm.ToValue(result)  
    }  

    // 将 Go 函数暴露给 JavaScript    vm.Set("hello", helloFunc)  

    // 在 JavaScript 中调用该函数  
    script := `  
        hello("Goja from FunTester");    `  
    result, err := vm.RunString(script)  
    if err != nil {  
       panic(err)  
    }  

    fmt.Println(result) // 输出: Hello, Goja from FunTester!  
}

这里我们将方法当做值传递给 JavaScript ,然后再在脚本中直接调用。

异常处理

JavaScript 中发生异常时,Goja 使用标准的 Go 错误处理机制来管理它。让我们通过下面这个例子来演示。

package main

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

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

    // 定义一个抛出异常的 JavaScript 代码
    script := `
        function errorFunc() {
            throw new Error("Something went wrong!");
        }
        errorFunc();
    `

    // 执行 JavaScript 代码并捕获错误
    _, err := vm.RunString(script)
    if err != nil {
        fmt.Println("Caught error:", err)
    }
}

基本思想是通过 RunString 执行 JavaScript 代码,并使用 Go 的错误处理机制来捕获 JavaScript 代码中的异常。这是一个经典的捕获 JavaScript 错误的示例。当 errorFunc 被调用时,它会抛出一个 JavaScript 错误,Go 通过 err 捕获这个错误,并打印出错误信息。

如果需要对捕获的异常做更详细的处理,可以进一步分析 err 变量。例如,可以将异常类型与消息进一步分离,以便做出针对性响应。

使用池化技术

在开发应用程序时,到初始化 VM 需要花费大量的性能。每个 VM 都需要加载全局模块,以便在运行时提供给用户。Go 提供的 sync.Pool 可以帮助重用对象,非常适合我的需求,避免了繁重的初始化开销。

以下是一个 Goja VM 池的示例:

package main

import (
    "fmt"
    "sync"

    "github.com/dop251/goja"
)

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

        // 定义每个 VM 都可以访问的全局函数
        vm.Set("add", func(a, b int) int {
            return a + b
        })

        // ... 设置其他全局值 ...

        return vm
    },
}

func main() {
    vm := vmPool.Get().(*goja.Runtime)
    // 将 VM 放回池中以供将来重用
    defer vmPool.Put(vm)

    // 运行自定义代码
    result, err := vm.RunString(`add(1, 2)`)
    if err != nil {
        panic(err)
    }
    fmt.Println(result) // 输出: 3
}

使用池来存储 VM 可以极大地提高性能,尤其是当我们需要频繁执行 JavaScript 代码时。这也是 K6 拥有高性能的原因。

结语

Goja 确实是一个非常灵活、轻量级且高效的 JavaScript 运行时,对于那些需要在 Go 语言环境中嵌入 JavaScript 的开发者来说,它提供了极大的便利和可能性。无论是简单的脚本执行、与 Go 代码的深度集成,还是在性能和内存使用方面的高效表现,Goja 都能为开发者带来很多益处。

如果你需要处理动态脚本执行、开发插件系统或构建更具扩展性的应用程序,Goja 绝对是一个值得尝试的工具。在简洁和性能之间,它找到了一种平衡,既保留了 Golang 本身的强大特性,也充分利用了 JavaScript 的灵活性。

FunTester 原创精华


↙↙↙阅读原文可查看相关链接,并与作者交流