searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

goja库简介

2025-12-12 05:36:00
2
0

概述

goja 是一个用纯 Go 编写的 ECMAScript 5.1+ 解释器。它允许在 Go 应用程序中执行 JavaScript 代码,提供了 Go 和 JavaScript 之间的双向互操作性。goja 旨在完全兼容 ECMAScript 5.1 规范,并支持部分 ES6 及更高版本的特性。它的设计目标是在 Go 环境中提供快速、可靠的 JavaScript 执行能力。

特性

  • 完整兼容性:支持完整的 ES5.1 规范,并通过测试套件验证。

  • 高性能:采用编译器和虚拟机架构,性能优异,接近 V8 等引擎。

  • 零依赖:纯 Go 实现,无需外部依赖,易于部署和交叉编译。

  • 内存安全:利用 Go 的垃圾回收机制,避免内存泄漏和悬挂指针。

  • 易于集成:简单的 API 设计,易于集成到现有 Go 项目中。

使用场景

  • 插件系统和脚本扩展:允许用户编写 JavaScript 脚本来扩展应用程序功能。

  • 规则引擎和表达式求值:动态执行规则和表达式,适用于业务规则频繁变化的场景。

  • 模板渲染和动态配置:使用 JavaScript 作为模板语言或动态配置脚本。

  • 测试框架中的自定义断言:在测试中执行 JavaScript 断言,适用于复杂逻辑验证。

  • 游戏脚本和逻辑分离:将游戏逻辑用 JavaScript 编写,便于热更新和修改。

核心概念

运行时环境

每个 goja 实例都是一个独立的 JavaScript 运行时环境,包含自己的全局对象、内置函数和对象。运行时环境之间相互隔离,一个运行时中的异常不会影响其他运行时。

值类型系统

goja 使用 goja.Value 接口表示 JavaScript 值,提供类型转换方法:

  • ToBoolean() bool:转换为布尔值。

  • ToInteger() int64:转换为整数。

  • ToFloat() float64:转换为浮点数。

  • ToString() string:转换为字符串。

  • Export() interface{}:将 JavaScript 值导出为 Go 值,例如对象转换为 map[string]interface{}。

错误处理

goja 返回两种类型的错误:

  1. JavaScript 异常(可通过 goja.Exception 获取,包含堆栈跟踪)。

  2. Go 运行时错误(如类型转换错误)。

 
func safeEval(vm *goja.Runtime, code string) (goja.Value, error) {
    defer func() {
        if r := recover(); r != nil {
            // 处理 panic,通常是 JavaScript 异常
            if exception, ok := r.(*goja.Exception); ok {
                fmt.Printf("JavaScript exception: %v\n", exception)
            } else {
                // 重新抛出非 JavaScript panic
                panic(r)
            }
        }
    }()
    return vm.RunString(code)
}

对象操作

// 创建对象
obj := vm.NewObject()
obj.Set("name", "John")
obj.Set("age", 30)

// 获取属性
name := obj.Get("name").String()

// 调用方法
method := obj.Get("toString")
if method != nil {
    result, _ := method.ToString().Call(obj)
}

// 遍历属性
for _, key := range obj.Keys() {
    value := obj.Get(key)
    fmt.Printf("%s: %v\n", key, value)
}

函数调用

goja 提供了多种调用 JavaScript 函数的方式:

 
// 方式1:使用 AssertFunction
fn, ok := goja.AssertFunction(vm.Get("add"))
if ok {
    result, err := fn(goja.Undefined(), vm.ToValue(1), vm.ToValue(2))
}

// 方式2:直接调用(如果确定是函数)
add := vm.Get("add")
result, err := add.Call(goja.Undefined(), 1, 2)

// 方式3:使用 RunString 调用
vm.RunString(`add(1, 2)`)

模块系统

goja 本身不提供模块系统,但可以通过设置模块加载器来实现:

go
vm.SetModuleLoader(func(name string) ([]byte, error) {
    // 从文件系统或其他源加载模块
    return os.ReadFile(name + ".js")
})

// 然后可以使用 require 函数(需要自行实现或使用 goja_nodejs)

API 参考

Runtime 类型

主要方法

go
// 创建运行时
vm := goja.New()

// 运行代码
vm.RunString(code string) (goja.Value, error)
vm.RunScript(name, code string) (goja.Value, error)

// 设置/获取全局变量
vm.Set(name string, value interface{})
vm.Get(name string) goja.Value

// 类型转换
vm.ToValue(val interface{}) goja.Value

// 创建对象和数组
vm.NewObject() *goja.Object
vm.NewArray() *goja.Object

运行时配置

go
// 设置内存限制(字节)
vm.SetMemoryLimit(limit int64)

// 设置中断通道,用于超时或取消
interrupt := make(chan func(), 1)
vm.SetInterrupt(interrupt)

// 发送中断信号
vm.Interrupt(func() {
    panic("interrupted")
})

值类型转换

从 Go 到 JavaScript

go
vm.ToValue(nil)              // null
vm.ToValue(true)             // boolean
vm.ToValue(42)               // number
vm.ToValue("hello")          // string
vm.ToValue([]interface{}{1,2,3}) // array
vm.ToValue(map[string]interface{}{"x":1}) // object

从 JavaScript 到 Go

go
val.Export() // 自动推断类型

// 指定类型
var x int
vm.ExportTo(val, &x)

// 导出为 map 或 slice
var m map[string]interface{}
vm.ExportTo(val, &m)

Promise 支持

goja 支持 Promise,但需要手动处理异步操作:

go
// 创建 Promise
promise, resolve, reject := vm.NewPromise()

// 在异步操作完成后调用 resolve 或 reject
go func() {
    time.Sleep(time.Second)
    resolve(vm.ToValue("done"))
}()

// 将 Promise 返回给 JavaScript
vm.Set("myAsyncFunc", func(call goja.FunctionCall) goja.Value {
    return vm.ToValue(promise)
})

注册 Go 函数

可以将 Go 函数注册为 JavaScript 函数:

go
vm.Set("add", func(call goja.FunctionCall) goja.Value {
    a := call.Argument(0).ToInteger()
    b := call.Argument(1).ToInteger()
    return vm.ToValue(a + b)
})

最佳实践

1. 资源管理

go
// 使用 defer 确保清理
func ProcessScript(code string) error {
    vm := goja.New()
    defer func() {
        // 清理资源
        vm.Interrupt("shutdown")
    }()
    
    // 设置超时
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    vm.SetInterrupt(make(chan func(), 1))
    go func() {
        <-ctx.Done()
        vm.Interrupt("timeout")
    }()
    
    _, err := vm.RunString(code)
    return err
}

2. 安全考虑

go
func CreateSandbox() *goja.Runtime {
    vm := goja.New()
    
    // 移除危险函数
    vm.Set("require", nil)
    vm.Set("eval", nil)
    
    // 限制访问
    vm.Set("console", createSafeConsole())
    
    // 限制内存
    vm.SetMemoryLimit(100 * 1024 * 1024) // 100MB
    
    return vm
}

func createSafeConsole() *goja.Object {
    obj := vm.NewObject()
    obj.Set("log", func(call goja.FunctionCall) goja.Value {
        // 安全的日志实现,限制输出长度
        for _, arg := range call.Arguments {
            str := arg.String()
            if len(str) > 1000 {
                str = str[:1000] + "..."
            }
            fmt.Println(str)
        }
        return goja.Undefined()
    })
    return obj
}

3. 性能优化

go
// 预编译脚本
func CompileAndCache(vm *goja.Runtime, code string) (*goja.Program, error) {
    program, err := goja.Compile("script.js", code, false)
    if err != nil {
        return nil, err
    }
    
    // 缓存编译结果
    return program, nil
}

// 复用运行时
type VMPool struct {
    pool sync.Pool
}

func NewVMPool() *VMPool {
    return &VMPool{
        pool: sync.Pool{
            New: func() interface{} {
                vm := goja.New()
                // 初始化预加载的库
                vm.RunString(preloadedLibraries)
                return vm
            },
        },
    }
}

func (p *VMPool) Get() *goja.Runtime {
    return p.pool.Get().(*goja.Runtime)
}

func (p *VMPool) Put(vm *goja.Runtime) {
    // 重置全局状态,避免污染
    vm.Set("__temp", nil)
    p.pool.Put(vm)
}

4. 调试和测试

go
// 添加调试支持
vm.SetDebugMode(true)

// 自定义错误处理
vm.SetErrorHandler(func(err error) {
    log.Printf("JavaScript error: %v", err)
    if jsErr, ok := err.(*goja.Exception); ok {
        log.Printf("Stack trace: %s", jsErr.String())
    }
})

// 单元测试辅助
func TestJavaScriptFunction(t *testing.T) {
    vm := goja.New()
    _, err := vm.RunString(`
        function add(a, b) { return a + b; }
    `)
    if err != nil {
        t.Fatal(err)
    }
    
    fn, _ := goja.AssertFunction(vm.Get("add"))
    result, _ := fn(nil, vm.ToValue(2), vm.ToValue(3))
    if result.ToInteger() != 5 {
        t.Errorf("Expected 5, got %d", result.ToInteger())
    }
}

5. 与 Go 代码交互

go
// 将 Go 结构体暴露给 JavaScript
type Person struct {
    Name string
    Age  int
}

func (p *Person) Greet() string {
    return "Hello, " + p.Name
}

vm.Set("Person", func(call goja.ConstructorCall) *goja.Object {
    // 从构造函数参数中获取数据
    var name string
    var age int
    if len(call.Arguments) > 0 {
        name = call.Argument(0).String()
    }
    if len(call.Arguments) > 1 {
        age = int(call.Argument(1).ToInteger())
    }
    
    p := &Person{Name: name, Age: age}
    
    // 将 Person 实例与 JavaScript 对象关联
    obj := call.This
    obj.Set("name", p.Name)
    obj.Set("age", p.Age)
    obj.Set("greet", func(call goja.FunctionCall) goja.Value {
        return vm.ToValue(p.Greet())
    })
    
    // 存储 Go 实例,以便在方法中访问
    obj.Set("__person", p)
    
    return obj
})
0条评论
作者已关闭评论
李****佳
10文章数
0粉丝数
李****佳
10 文章 | 0 粉丝
李****佳
10文章数
0粉丝数
李****佳
10 文章 | 0 粉丝
原创

goja库简介

2025-12-12 05:36:00
2
0

概述

goja 是一个用纯 Go 编写的 ECMAScript 5.1+ 解释器。它允许在 Go 应用程序中执行 JavaScript 代码,提供了 Go 和 JavaScript 之间的双向互操作性。goja 旨在完全兼容 ECMAScript 5.1 规范,并支持部分 ES6 及更高版本的特性。它的设计目标是在 Go 环境中提供快速、可靠的 JavaScript 执行能力。

特性

  • 完整兼容性:支持完整的 ES5.1 规范,并通过测试套件验证。

  • 高性能:采用编译器和虚拟机架构,性能优异,接近 V8 等引擎。

  • 零依赖:纯 Go 实现,无需外部依赖,易于部署和交叉编译。

  • 内存安全:利用 Go 的垃圾回收机制,避免内存泄漏和悬挂指针。

  • 易于集成:简单的 API 设计,易于集成到现有 Go 项目中。

使用场景

  • 插件系统和脚本扩展:允许用户编写 JavaScript 脚本来扩展应用程序功能。

  • 规则引擎和表达式求值:动态执行规则和表达式,适用于业务规则频繁变化的场景。

  • 模板渲染和动态配置:使用 JavaScript 作为模板语言或动态配置脚本。

  • 测试框架中的自定义断言:在测试中执行 JavaScript 断言,适用于复杂逻辑验证。

  • 游戏脚本和逻辑分离:将游戏逻辑用 JavaScript 编写,便于热更新和修改。

核心概念

运行时环境

每个 goja 实例都是一个独立的 JavaScript 运行时环境,包含自己的全局对象、内置函数和对象。运行时环境之间相互隔离,一个运行时中的异常不会影响其他运行时。

值类型系统

goja 使用 goja.Value 接口表示 JavaScript 值,提供类型转换方法:

  • ToBoolean() bool:转换为布尔值。

  • ToInteger() int64:转换为整数。

  • ToFloat() float64:转换为浮点数。

  • ToString() string:转换为字符串。

  • Export() interface{}:将 JavaScript 值导出为 Go 值,例如对象转换为 map[string]interface{}。

错误处理

goja 返回两种类型的错误:

  1. JavaScript 异常(可通过 goja.Exception 获取,包含堆栈跟踪)。

  2. Go 运行时错误(如类型转换错误)。

 
func safeEval(vm *goja.Runtime, code string) (goja.Value, error) {
    defer func() {
        if r := recover(); r != nil {
            // 处理 panic,通常是 JavaScript 异常
            if exception, ok := r.(*goja.Exception); ok {
                fmt.Printf("JavaScript exception: %v\n", exception)
            } else {
                // 重新抛出非 JavaScript panic
                panic(r)
            }
        }
    }()
    return vm.RunString(code)
}

对象操作

// 创建对象
obj := vm.NewObject()
obj.Set("name", "John")
obj.Set("age", 30)

// 获取属性
name := obj.Get("name").String()

// 调用方法
method := obj.Get("toString")
if method != nil {
    result, _ := method.ToString().Call(obj)
}

// 遍历属性
for _, key := range obj.Keys() {
    value := obj.Get(key)
    fmt.Printf("%s: %v\n", key, value)
}

函数调用

goja 提供了多种调用 JavaScript 函数的方式:

 
// 方式1:使用 AssertFunction
fn, ok := goja.AssertFunction(vm.Get("add"))
if ok {
    result, err := fn(goja.Undefined(), vm.ToValue(1), vm.ToValue(2))
}

// 方式2:直接调用(如果确定是函数)
add := vm.Get("add")
result, err := add.Call(goja.Undefined(), 1, 2)

// 方式3:使用 RunString 调用
vm.RunString(`add(1, 2)`)

模块系统

goja 本身不提供模块系统,但可以通过设置模块加载器来实现:

go
vm.SetModuleLoader(func(name string) ([]byte, error) {
    // 从文件系统或其他源加载模块
    return os.ReadFile(name + ".js")
})

// 然后可以使用 require 函数(需要自行实现或使用 goja_nodejs)

API 参考

Runtime 类型

主要方法

go
// 创建运行时
vm := goja.New()

// 运行代码
vm.RunString(code string) (goja.Value, error)
vm.RunScript(name, code string) (goja.Value, error)

// 设置/获取全局变量
vm.Set(name string, value interface{})
vm.Get(name string) goja.Value

// 类型转换
vm.ToValue(val interface{}) goja.Value

// 创建对象和数组
vm.NewObject() *goja.Object
vm.NewArray() *goja.Object

运行时配置

go
// 设置内存限制(字节)
vm.SetMemoryLimit(limit int64)

// 设置中断通道,用于超时或取消
interrupt := make(chan func(), 1)
vm.SetInterrupt(interrupt)

// 发送中断信号
vm.Interrupt(func() {
    panic("interrupted")
})

值类型转换

从 Go 到 JavaScript

go
vm.ToValue(nil)              // null
vm.ToValue(true)             // boolean
vm.ToValue(42)               // number
vm.ToValue("hello")          // string
vm.ToValue([]interface{}{1,2,3}) // array
vm.ToValue(map[string]interface{}{"x":1}) // object

从 JavaScript 到 Go

go
val.Export() // 自动推断类型

// 指定类型
var x int
vm.ExportTo(val, &x)

// 导出为 map 或 slice
var m map[string]interface{}
vm.ExportTo(val, &m)

Promise 支持

goja 支持 Promise,但需要手动处理异步操作:

go
// 创建 Promise
promise, resolve, reject := vm.NewPromise()

// 在异步操作完成后调用 resolve 或 reject
go func() {
    time.Sleep(time.Second)
    resolve(vm.ToValue("done"))
}()

// 将 Promise 返回给 JavaScript
vm.Set("myAsyncFunc", func(call goja.FunctionCall) goja.Value {
    return vm.ToValue(promise)
})

注册 Go 函数

可以将 Go 函数注册为 JavaScript 函数:

go
vm.Set("add", func(call goja.FunctionCall) goja.Value {
    a := call.Argument(0).ToInteger()
    b := call.Argument(1).ToInteger()
    return vm.ToValue(a + b)
})

最佳实践

1. 资源管理

go
// 使用 defer 确保清理
func ProcessScript(code string) error {
    vm := goja.New()
    defer func() {
        // 清理资源
        vm.Interrupt("shutdown")
    }()
    
    // 设置超时
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    vm.SetInterrupt(make(chan func(), 1))
    go func() {
        <-ctx.Done()
        vm.Interrupt("timeout")
    }()
    
    _, err := vm.RunString(code)
    return err
}

2. 安全考虑

go
func CreateSandbox() *goja.Runtime {
    vm := goja.New()
    
    // 移除危险函数
    vm.Set("require", nil)
    vm.Set("eval", nil)
    
    // 限制访问
    vm.Set("console", createSafeConsole())
    
    // 限制内存
    vm.SetMemoryLimit(100 * 1024 * 1024) // 100MB
    
    return vm
}

func createSafeConsole() *goja.Object {
    obj := vm.NewObject()
    obj.Set("log", func(call goja.FunctionCall) goja.Value {
        // 安全的日志实现,限制输出长度
        for _, arg := range call.Arguments {
            str := arg.String()
            if len(str) > 1000 {
                str = str[:1000] + "..."
            }
            fmt.Println(str)
        }
        return goja.Undefined()
    })
    return obj
}

3. 性能优化

go
// 预编译脚本
func CompileAndCache(vm *goja.Runtime, code string) (*goja.Program, error) {
    program, err := goja.Compile("script.js", code, false)
    if err != nil {
        return nil, err
    }
    
    // 缓存编译结果
    return program, nil
}

// 复用运行时
type VMPool struct {
    pool sync.Pool
}

func NewVMPool() *VMPool {
    return &VMPool{
        pool: sync.Pool{
            New: func() interface{} {
                vm := goja.New()
                // 初始化预加载的库
                vm.RunString(preloadedLibraries)
                return vm
            },
        },
    }
}

func (p *VMPool) Get() *goja.Runtime {
    return p.pool.Get().(*goja.Runtime)
}

func (p *VMPool) Put(vm *goja.Runtime) {
    // 重置全局状态,避免污染
    vm.Set("__temp", nil)
    p.pool.Put(vm)
}

4. 调试和测试

go
// 添加调试支持
vm.SetDebugMode(true)

// 自定义错误处理
vm.SetErrorHandler(func(err error) {
    log.Printf("JavaScript error: %v", err)
    if jsErr, ok := err.(*goja.Exception); ok {
        log.Printf("Stack trace: %s", jsErr.String())
    }
})

// 单元测试辅助
func TestJavaScriptFunction(t *testing.T) {
    vm := goja.New()
    _, err := vm.RunString(`
        function add(a, b) { return a + b; }
    `)
    if err != nil {
        t.Fatal(err)
    }
    
    fn, _ := goja.AssertFunction(vm.Get("add"))
    result, _ := fn(nil, vm.ToValue(2), vm.ToValue(3))
    if result.ToInteger() != 5 {
        t.Errorf("Expected 5, got %d", result.ToInteger())
    }
}

5. 与 Go 代码交互

go
// 将 Go 结构体暴露给 JavaScript
type Person struct {
    Name string
    Age  int
}

func (p *Person) Greet() string {
    return "Hello, " + p.Name
}

vm.Set("Person", func(call goja.ConstructorCall) *goja.Object {
    // 从构造函数参数中获取数据
    var name string
    var age int
    if len(call.Arguments) > 0 {
        name = call.Argument(0).String()
    }
    if len(call.Arguments) > 1 {
        age = int(call.Argument(1).ToInteger())
    }
    
    p := &Person{Name: name, Age: age}
    
    // 将 Person 实例与 JavaScript 对象关联
    obj := call.This
    obj.Set("name", p.Name)
    obj.Set("age", p.Age)
    obj.Set("greet", func(call goja.FunctionCall) goja.Value {
        return vm.ToValue(p.Greet())
    })
    
    // 存储 Go 实例,以便在方法中访问
    obj.Set("__person", p)
    
    return obj
})
文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0