在Go中使用QJS和WebAssembly运行现代ES2023 JavaScript

来源: InfoQ - 后端

原文

QJS是一个不依赖CGO的现代JavaScript运行时,用于Go语言,它将QuickJS引擎嵌入到WebAssembly模块中,并使用Wazero运行它,为Go应用程序提供了一个带有async/await和和紧密Go-JS互操作性的沙箱化ES2023环境。

QJS的目标是那些希望在Go进程中运行现代JavaScript而不链接本地C库的Go开发人员。它没有直接通过CGO绑定QuickJS,而是将QuickJS-NG编译为WebAssembly,并在Wazero下执行,提供:

完整的ES2023支持(模块、async/await、BigInt等)。

一个完全沙箱化、内存安全的执行模型。

无需CGO工具链或C运行时依赖。

该运行时与Go 1.22+兼容,并作为常规Go模块分发:

go get github.com/fastschema/qjs

然后:

import "github.com/fastschema/qjs"

QJS公开了一个 Runtime 和 Context API,允许Go代码评估JavaScript、绑定函数和交换数据结构。一个最简单的示例创建了一个运行时,计算脚本,并将结构化结果读回到Go中:

rt, err := qjs.New()
 if err != nil {
     log.Fatal(err)
 }
 defer rt.Close()
ctx := rt.Context()
result, err := ctx.Eval("test.js", qjs.Code(`
     const person = {
         name: "Alice",
         age: 30,
         city: "New York"
     };
const info = Object.keys(person).map(key =>
    key + ": " + person[key]
).join(", ");


({ person: person, info: info });
))


 if err != nil {
     log.Fatal("Eval error:", err)
 }
 defer result.Free()
log.Println(result.GetPropertyStr("info").String())
 log.Println(result.GetPropertyStr("person").GetPropertyStr("name").String())
 log.Println(result.GetPropertyStr("person").GetPropertyStr("age").Int32())

Go函数可以暴露给JavaScript,JS函数可以转换回类型化的Go可调用函数。例如,绑定一个Go函数:

ctx.SetFunc("goFunction", func(this qjs.This) (qjs.Value, error) {
     return this.Context().NewString("Hello from Go!"), nil
 })
result, err := ctx.Eval("test.js", qjs.Code(    
 const message = goFunction();    
  message; 
))
 if err != nil {
     log.Fatal("Eval error:", err)
 }
 defer result.Free()
log.Println(result.String()) // Hello from Go!

QJS还支持将更丰富的Go结构体转换为JS值,包括可以从JavaScript调用的方法,然后反序列化为类型化的Go值。

为了避免重复序列化大型或不透明的Go对象,QJS引入了Proxy,这是一个轻量级的JavaScript包装器,只保存对Go值的引用。这对于上下文、数据库句柄或JS不需要检查就可以通过的大型结构体来说很有用:

ctx.SetFunc("$context", func(this qjs.This) (qjs.Value, error) {
     passContext := context.WithValue(context.Background(), "key", "value123")
     val := ctx.NewProxyValue(passContext)
     return val, nil
 })
goFuncWithContext := func(c context.Context, num int) int {
     log.Println("Context value:", c.Value("key"))
     return num * 2
 }

JavaScript接收代理并将其传回Go,其中JsValueToGo恢复底层值和类型。QJS通过允许Go异步解决JS承诺来支持async/await。一个Go异步函数可以调度工作并解决一个承诺:

ctx.SetAsyncFunc("asyncFunction", func(this *qjs.This) {
  go func() {
     time.Sleep(100 * time.Millisecond)
     result := this.Context().NewString("Async result from Go!")   
     this.Promise().Resolve(result)  
    }()
})

然后JS await它:

async function main() {
  const result = await asyncFunction();
   return result; 
}
({ main: main() });

该运行时可用于在JavaScript中实现HTTP处理程序,同时保持服务器在Go中。例如, /about 和 /contact 路由在JS中定义,预编译为字节码,并从运行时池中执行:

byteCode := must(ctx.Compile("script.js", qjs.Code(script), qjs.TypeModule()))
 pool := qjs.NewPool(3, &qjs.Option{}, func(r *qjs.Runtime) error {
     results := must(r.Context().Eval("script.js", qjs.Bytecode(byteCode), qjs.TypeModule()))
     r.Context().Global().SetPropertyStr("handlers", results)
     return nil
 })
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
     runtime := must(pool.Get())
     defer pool.Put(runtime)
handlers := runtime.Context().Global().GetPropertyStr("handlers")
result := must(handlers.InvokeJS("about"))
fmt.Fprint(w, result.String())
result.Free()
})

计算阶乘(10)1,000,000次

AreWeFastYet V8-V7

通过这种设计,QJS的目标是那些需要安全的插件系统、用户提供的脚本或用JavaScript编写的嵌入式业务逻辑,而不需要将C工具链或CGO引入构建和部署流水线的Go开发人员。

原文链接:

https://www.infoq.com/news/2025/12/javascript-golang-wasm/