当前位置:首页 > 文章列表 > Golang > Go教程 > Go Goja 怎么预编译脚本并在多个运行时执行

Go Goja 怎么预编译脚本并在多个运行时执行

来源:17golang原创 2026-10-13 15:23:56 0浏览 收藏

如果一段 JavaScript 规则会被反复用于不同请求,直接对每个请求调用 RunString 会把解析和编译也带进每次执行。Goja 提供的 goja.Compile 可以先生成一个与运行时无关的 *goja.Program,之后把这个 Program 交给多个独立的 Runtime 使用。

官方地址:https://github.com/dop251/goja

正确的复用单位是“共享只读的 Program,隔离每个 Runtime 的状态”。Program 可以被多个 Runtime 使用,但 Runtime 本身不能被多个 goroutine 同时操作;请求数据也应通过各自 Runtime 的输入绑定注入。

先分清 Program 与 Runtime 的职责

Compile(name, src, strict) 负责把脚本文本转换成 Goja 的内部编译表示;Runtime.RunProgram 再在某个 Runtime 的全局上下文中执行它。Program 不绑定某一个 Runtime,因此可以缓存或被多个 Runtime 读取;变量、对象、全局函数和执行结果则属于具体 Runtime。

这一区分很重要:如果把 Runtime 放进全局变量后让多个请求同时调用,问题不是“Program 没有缓存”,而是运行时状态被并发访问。Goja 官方资料明确说明,一个 Runtime 同时只能由一个 goroutine 使用。

Goja Compile 生成可共享 Program,多个独立 Runtime 分别持有执行状态的静态结构说明图
图1:Program 与多个 Runtime 的静态关系;Program 位于共享编译边界,Runtime A 与 Runtime B 各自承载全局对象和执行状态,这是一张结构说明图,不是运行截图。

准备模块并编译一次脚本

先创建一个最小 Go module,并安装 Goja。官方仓库当前 README 还列出了它的 Go 版本要求,实际项目应以仓库当前说明和自身锁定的依赖版本为准。

# 创建实验目录并初始化 Go module。
mkdir goja-program-demo
cd goja-program-demo
go mod init example.com/goja-program-demo

# 引入 Goja;版本由当前 module 的依赖解析结果记录到 go.mod。
go get github.com/dop251/goja

把脚本的职责限制在“读取 input 并返回一个对象”。这样可以清楚看到:编译只依赖源代码,业务数据在 Runtime 中注入。

package main

import (
	"fmt"

	"github.com/dop251/goja"
)

const script = `
function quote(input) {
    // 只读取当前 Runtime 中的 input,不把请求数据写入共享 Program。
    return {
        id: input.id,
        total: input.price * input.quantity
    };
}

quote(input);
`

// compileProgram 只负责编译脚本文本;语法错误应在初始化或加载阶段暴露。
func compileProgram() (*goja.Program, error) {
	return goja.Compile("quote.js", script, false)
}

func main() {
	program, err := compileProgram()
	if err != nil {
		// 编译失败时没有可执行的 Program,调用方应停止发布或加载。
		panic(err)
	}

	fmt.Printf("compiled program: %T\n", program)
}

这里使用 Compile 而不是 MustCompile,是因为脚本可能来自配置、数据库或管理端输入。初始化阶段可以把错误记录清楚;只有源码随程序一起发布且编译失败代表开发错误时,才适合考虑 MustCompile。

在多个 Runtime 中传入数据并执行

编译得到 Program 后,每次执行都新建一个 Runtime,把当前请求的 input 放入该 Runtime,再调用 RunProgram。下面的辅助函数不共享 Runtime,因此既可以顺序调用,也可以由多个 goroutine 分别调用。

// runProgram 为一次执行创建独立 Runtime,避免跨请求共享全局对象。
func runProgram(program *goja.Program, input map[string]any) (any, error) {
	vm := goja.New()
	if err := vm.Set("input", input); err != nil {
		// 输入转换失败时返回错误,不让错误数据继续进入脚本。
		return nil, fmt.Errorf("set input: %w", err)
	}

	value, err := vm.RunProgram(program)
	if err != nil {
		// JavaScript 异常属于运行错误,与 Compile 阶段的语法错误分开处理。
		return nil, fmt.Errorf("run program: %w", err)
	}
	return value.Export(), nil
}

func main() {
	program, err := compileProgram()
	if err != nil {
		panic(err)
	}

	inputs := []map[string]any{
		{"id": "A-100", "price": 12.5, "quantity": 2},
		{"id": "B-200", "price": 8.0, "quantity": 3},
	}
	for _, input := range inputs {
		result, err := runProgram(program, input)
		if err != nil {
			fmt.Println(err)
			continue
		}
		// 结果属于本次 Runtime,导出后交给 Go 业务层继续处理。
		fmt.Printf("%v\n", result)
	}
}

这个例子里只编译一次脚本,两个输入分别进入两个生命周期独立的 Runtime。value.Export() 把脚本返回的对象转换为 Go 值;如果希望得到严格的结构体,可以在导出阶段使用 Runtime.ExportTo,而不是把 Runtime 或 JavaScript 对象跨请求保存。

每个 Goja Runtime 独立接收 input、调用 RunProgram 并导出 result 的静态数据关系图
图2:单次执行的数据边界说明图;输入只进入对应 Runtime,RunProgram 读取共享 Program,结果从该 Runtime 导出到 Go,这是一张静态说明图,不是运行截图。

检查错误、并发边界与状态隔离

把执行路径拆开后,错误可以按来源处理:

错误位置典型表现处理建议
Compile脚本文法或编译选项不合法拒绝缓存该 Program,并记录脚本来源
vm.Set输入无法转换到 Goja 值在进入脚本前返回输入错误
RunProgram脚本抛异常或访问了不存在的数据把它作为本次请求的运行失败处理
Export / ExportTo结果结构与 Go 目标类型不匹配在边界层返回转换错误,不复用脏结果

如果要并发执行,同一个 program 可以被多个调用读取,但每个调用都应拥有自己的 vm := goja.New()。不要把同一个 Runtime 放入 sync.Pool 后直接交给多个 goroutine;即使通过池化降低创建频率,也必须保证一次只由一个 goroutine 使用,并在归还前清理会影响下一次请求的全局变量。

// 并发调用时共享 Program,但每个任务仍通过 runProgram 创建自己的 Runtime。
func runBatch(program *goja.Program, inputs []map[string]any) {
	var wg sync.WaitGroup
	for _, input := range inputs {
		input := input
		wg.Add(1)
		go func() {
			defer wg.Done()
			// 不把 vm 提到循环外,避免多个 goroutine 共享 Runtime。
			result, err := runProgram(program, input)
			if err != nil {
				fmt.Println(err)
				return
			}
			fmt.Println(result)
		}()
	}
	wg.Wait()
}

这段并发片段需要在文件导入 sync,并沿用前面的 runProgram。输出顺序不应作为业务协议,因为 goroutine 完成顺序是不固定的;如果业务需要稳定顺序,应在 Go 层给任务编号并收集结果。

扩展为缓存和受控复用

实际服务可以把脚本源文本映射到 *goja.Program 缓存,但缓存键要包含会改变语义的内容,例如脚本版本、严格模式选项或租户边界。编译成功后再写入缓存,编译失败不能用旧的半成品覆盖有效 Program。

Runtime 是否池化要单独决定。Program 复用通常只要求保存一个只读对象;Runtime 池化则要求完整清理全局状态、回调、定时器和业务注入对象,排查成本更高。若单次脚本很短,优先保持“一个执行一个 Runtime”的清晰模型,再用真实业务基准判断创建成本是否值得优化。

还要留意脚本执行本身可能耗时过长。Goja 官方仓库提供了 Runtime.Interrupt 相关能力,但中断策略需要结合服务的取消、超时和错误回收方案设计;不要只把 goroutine 停止当成脚本已经安全结束。

清理与总结

  • 用 goja.Compile 把稳定脚本文本转换成可缓存的 *goja.Program。
  • 每次请求创建独立 Runtime,通过 Set 注入输入,再调用 RunProgram。
  • Program 可以在多个 Runtime 间共享;Runtime 不能被多个 goroutine 同时使用。
  • 分别处理编译错误、输入转换错误、JavaScript 运行错误和结果导出错误。
  • 只有在能清理状态并有基准数据时,才把 Runtime 放进池化复用方案。

相关问题

Program 可以在多个 Runtime 中同时执行吗?

可以。Program 与 Runtime 解耦,多个 Runtime 可以分别调用 RunProgram;并发时仍需保证每个 goroutine 使用自己的 Runtime。

为什么不直接缓存 Runtime?

Runtime 内含全局对象和执行状态,缓存后必须处理并发互斥、请求数据残留和异常恢复。缓存 Program 只复用编译结果,隔离边界更清晰。

RunProgram 和 RunString 有什么区别?

RunString 接收脚本文本并执行,适合一次性脚本;RunProgram 接收已经由 Compile 生成的 Program,适合同一脚本被反复执行的路径。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go GopherLua CallByParam 的保护调用和普通调用有什么区别Go GopherLua CallByParam 的保护调用和普通调用有什么区别
上一篇
Go GopherLua CallByParam 的保护调用和普通调用有什么区别
Go Goja 怎么把 JavaScript 对象导出到 Go 结构体
下一篇
Go Goja 怎么把 JavaScript 对象导出到 Go 结构体
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    460次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    521次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    539次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    487次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    307次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码