概述
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 返回两种类型的错误:
-
JavaScript 异常(可通过
goja.Exception获取,包含堆栈跟踪)。 -
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 本身不提供模块系统,但可以通过设置模块加载器来实现:
vm.SetModuleLoader(func(name string) ([]byte, error) { // 从文件系统或其他源加载模块 return os.ReadFile(name + ".js") }) // 然后可以使用 require 函数(需要自行实现或使用 goja_nodejs)
API 参考
Runtime 类型
主要方法
// 创建运行时 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
运行时配置
// 设置内存限制(字节) vm.SetMemoryLimit(limit int64) // 设置中断通道,用于超时或取消 interrupt := make(chan func(), 1) vm.SetInterrupt(interrupt) // 发送中断信号 vm.Interrupt(func() { panic("interrupted") })
值类型转换
从 Go 到 JavaScript
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
val.Export() // 自动推断类型 // 指定类型 var x int vm.ExportTo(val, &x) // 导出为 map 或 slice var m map[string]interface{} vm.ExportTo(val, &m)
Promise 支持
goja 支持 Promise,但需要手动处理异步操作:
// 创建 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 函数:
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. 资源管理
// 使用 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. 安全考虑
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. 性能优化
// 预编译脚本 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. 调试和测试
// 添加调试支持 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 结构体暴露给 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 })