Goja—Go 和 JavaScript 的橋樑

FunTester發表於2024-10-11

在現代軟體開發中,嵌入式指令碼引擎為開發者提供了極大的靈活性。無論是需要動態擴充套件的應用程式,還是需要跨語言整合的專案,指令碼引擎的引入讓複雜系統得以更加靈活地運作。在 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 原創精華
  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章